Testing Laravel Jobs and email | Laravel Testing

Testing Laravel Jobs and email | Laravel Testing

Posted on:October 21, 2023 at 08:00 PM

Hello Everyone

You might not get so much tutorial about laravel job testing. Therefore I would like to write a post where you can get deep understanding of how to test laravel job and what to test and not.

Table of Content

Context

Imagine that I have a job that takes an email as a parameter when it’s being called. The job check whether this email is already in our subscribers table or not. It does not if found in the table, otherwise add the email in the table and then send an welcome email.

Write Tests

It’s always tricky that what I should test. However I plan for them because these are the main behavior for our context:

  • it_adds_new_subscriber_and_sends_email_if_not_already_subscribed
  • it_does_nothing_if_email_is_already_subscribed

Write a test class

To write a test class, follow this command:

php artisan make:test ProcessSubscriberEmailTest

Let’s write first test for it_adds_new_subscriber_and_sends_email_if_not_already_subscribed

use Illuminate\Support\Facades\Mail;
use App\Jobs\ProcessSubscriberEmail;

/** @test */
public function it_adds_new_subscriber_and_sends_email_if_not_already_subscribed()
{
    // ARRANGE :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    // Using Mail fake facade to confirm that the real email is not sending during testing
    Mail::fake();

    // Confirm the job run sync
    config(['queue.default' => 'sync']);

    // Expected email address to send email
    $email = '[email protected]';

    // Pre-assertion
    $this->assertDatabaseCount('subscribers', 0);

    // ACT :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    ProcessSubscriberEmail::dispatch($email); // Dispatch the event

    // ASSERT ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    $this->assertDatabaseCount('subscribers', 1);
    $this->assertDatabaseHas('subscribers', ['email' => $email]);

    // Confirm that email send to the expected email
    Mail::assertSent(WelcomeSubscriber::class, function ($mail) use ($email) {
        return $mail->hasTo($email);
        // Write more assertion if you want confirm the content of the email because my main focus is to write the test only!
        // Check more: https://laravel.com/docs/10.x/mail#testing-mailable-content
    });
}

Now let’s write the opposite behavior it_does_nothing_if_email_is_already_subscribed.


/** @test */
public function it_does_nothing_if_email_is_already_subscribed()
{
    // ARRANGE :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    Mail::fake();

    $email = '[email protected]';
    Subscriber::create(['email' => $email]);

    // Confirm the job run sync
    config(['queue.default' => 'sync']);

    // Pre-assertion
    $this->assertDatabaseCount('subscribers', 1);

    // ACT :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    ProcessSubscriberEmail::dispatch($email);

    // ASSERT ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    Mail::assertNotSent(WelcomeSubscriber::class);
    $this->assertDatabaseCount('subscribers', 1);
}

Notes:

  • ⚠️ Don’t forget to use database trait right after declare the test class. i.g. use RefreshDatabase.

Full Test Code:

namespace Tests\Feature;

use App\Jobs\ProcessSubscriberEmail;
use App\Models\Subscriber;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class ProcessSubscriberEmailTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_adds_new_subscriber_and_sends_email_if_not_already_subscribed()
    {
        // ARRANGE :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
        // Using Mail fake facade to confirm that the real email is not sending during testing
        Mail::fake();

        // Confirm the job run sync
        config(['queue.default' => 'sync']);

        // Expected email address to send email
        $email = '[email protected]';

        // Pre-assertion
        $this->assertDatabaseCount('subscribers', 0);

        // ACT :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
        ProcessSubscriberEmail::dispatch($email); // Dispatch the event

        // ASSERT ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
        $this->assertDatabaseCount('subscribers', 1);
        $this->assertDatabaseHas('subscribers', ['email' => $email]);

        // Confirm that email send to the expected email
        Mail::assertSent(WelcomeSubscriber::class, function ($mail) use ($email) {
            return $mail->hasTo($email);
            // Write more assertion if you want confirm the content of the email
            // Check more: https://laravel.com/docs/10.x/mail#testing-mailable-content
        });
    }

    /** @test */
    public function it_does_nothing_if_email_is_already_subscribed()
    {
        // ARRANGE :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
        Mail::fake();

        $email = '[email protected]';
        Subscriber::create(['email' => $email]);

        // Confirm the job run sync
        config(['queue.default' => 'sync']);

        // Pre-assertion
        $this->assertDatabaseCount('subscribers', 1);

        // ACT :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
        ProcessSubscriberEmail::dispatch($email);

        // ASSERT ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
        Mail::assertNotSent(WelcomeSubscriber::class);
        $this->assertDatabaseCount('subscribers', 1);
    }
}

Real Implementation

Here is my real implementation which I would like to put it in the collapsible area because we are focusing on the test section only.


class ProcessSubscriberEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $email;

    public function __construct(string $email)
    {
        $this->email = $email;
    }

    public function handle()
    {
        // Check if the email is already in the subscribers table
        if (!Subscriber::where('email', $this->email)->exists()) {
            // Add a new entry in the subscribers table
            Subscriber::create(['email' => $this->email]);
            
            // Send the email
            Mail::to($this->email)->send(new WelcomeSubscriber());
        }
    }
}

Run the test

To run the test, finally run this command:

php artisan test

or

vendor/bin/phpunit

We any luck, you will see all of your test are passing.

If you have any question, or comments to make it better, feel free to share your thoughts. I am happy to discuss further.

Happy Coding.