Testing Laravel Jobs and email | Laravel Testing
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.