Testing Email & Notifications
Email and notification testing is crucial for ensuring that your application communicates effectively with users. In this lesson, we'll explore comprehensive strategies for testing mail, notifications, queues, and broadcasts without actually sending emails or triggering real notifications during tests.
Why Test Communications?
Testing email and notification systems ensures:
- Correct recipients receive messages
- Message content is accurate and properly formatted
- Attachments and embedded images work correctly
- Queue jobs are dispatched properly
- Notification channels (mail, SMS, Slack, etc.) function as expected
- Broadcasting events reach the correct channels
Important: Never send real emails or notifications during automated tests. Always use fakes or mocking to prevent spam and protect user data.
Laravel Mail Testing Basics
Laravel provides the Mail facade with a powerful fake() method that intercepts all outgoing mail during tests:
<?php
namespace Tests\Feature;
use App\Mail\WelcomeEmail;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class UserRegistrationTest extends TestCase
{
use RefreshDatabase;
public function test_welcome_email_is_sent_on_registration()
{
// Prevent actual email sending
Mail::fake();
// Perform registration
$response = $this->post('/register', [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
// Assert email was sent
Mail::assertSent(WelcomeEmail::class, function ($mail) {
return $mail->hasTo('john@example.com') &&
$mail->hasSubject('Welcome to Our Platform');
});
}
}
Advanced Mail Assertions
Laravel provides numerous assertion methods for testing emails:
// Assert specific mailable was sent
Mail::assertSent(OrderShipped::class);
// Assert mailable was sent to specific user
Mail::assertSent(OrderShipped::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
// Assert mailable was sent N times
Mail::assertSent(InvoiceGenerated::class, 3);
// Assert mailable was NOT sent
Mail::assertNotSent(CancelledOrder::class);
// Assert no mailables were sent
Mail::assertNothingSent();
// Assert queued (not sent immediately)
Mail::assertQueued(NewsletterEmail::class);
// Assert mailable has specific data
Mail::assertSent(OrderShipped::class, function ($mail) {
return $mail->order->id === 123 &&
$mail->hasAttachment('/path/to/invoice.pdf');
});
Testing Mailable Content
You can render mailables directly in tests to verify their content:
<?php
namespace Tests\Unit;
use App\Mail\OrderConfirmation;
use App\Models\Order;
use Tests\TestCase;
class OrderConfirmationMailTest extends TestCase
{
public function test_email_contains_order_details()
{
$order = Order::factory()->create([
'total' => 99.99,
'order_number' => 'ORD-12345',
]);
$mailable = new OrderConfirmation($order);
// Render email HTML
$content = $mailable->render();
// Assert content
$this->assertStringContainsString('ORD-12345', $content);
$this->assertStringContainsString('$99.99', $content);
$this->assertStringContainsString('Order Confirmation', $content);
}
public function test_email_has_correct_subject_and_from()
{
$order = Order::factory()->create();
$mailable = new OrderConfirmation($order);
$mailable->assertSeeInHtml('Order Confirmation');
$mailable->assertFrom('orders@example.com');
$mailable->assertHasSubject('Your Order #' . $order->order_number);
}
}
Testing Notifications
Laravel's notification system can be tested similarly using the Notification facade:
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Notifications\AccountVerified;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
class EmailVerificationTest extends TestCase
{
use RefreshDatabase;
public function test_notification_sent_on_email_verification()
{
Notification::fake();
$user = User::factory()->create([
'email_verified_at' => null,
]);
// Verify email
$this->actingAs($user)
->post('/email/verify');
// Assert notification was sent
Notification::assertSentTo(
$user,
AccountVerified::class
);
}
public function test_notification_sent_via_correct_channels()
{
Notification::fake();
$user = User::factory()->create();
$user->notify(new AccountVerified());
Notification::assertSentTo($user, AccountVerified::class,
function ($notification, $channels) {
return in_array('mail', $channels) &&
in_array('database', $channels);
}
);
}
}
Testing Multiple Notification Channels
When testing notifications that use multiple channels (mail, SMS, Slack, etc.), verify each channel independently:
<?php
public function test_urgent_notification_sent_via_all_channels()
{
Notification::fake();
$admin = User::factory()->create(['role' => 'admin']);
// Trigger urgent notification
$this->post('/incidents/create', [
'severity' => 'critical',
'message' => 'Server down',
]);
Notification::assertSentTo($admin, UrgentIncident::class,
function ($notification, $channels) {
// Verify all three channels
return count($channels) === 3 &&
in_array('mail', $channels) &&
in_array('sms', $channels) &&
in_array('slack', $channels);
}
);
}
public function test_notification_contains_correct_data()
{
Notification::fake();
$user = User::factory()->create();
$order = Order::factory()->create();
$user->notify(new OrderShipped($order));
Notification::assertSentTo($user, OrderShipped::class,
function ($notification) use ($order) {
$mailData = $notification->toMail($user);
return $mailData->subject === 'Order Shipped' &&
$notification->order->id === $order->id;
}
);
}
Testing Queue Jobs
Many emails and notifications are sent via queues. Test queue dispatching separately:
<?php
namespace Tests\Feature;
use App\Jobs\SendBulkEmails;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class BulkEmailTest extends TestCase
{
use RefreshDatabase;
public function test_bulk_email_job_is_dispatched()
{
Queue::fake();
$users = User::factory()->count(100)->create();
// Trigger bulk email
$this->post('/admin/send-newsletter', [
'subject' => 'Monthly Update',
'body' => 'Newsletter content...',
]);
// Assert job was pushed to queue
Queue::assertPushed(SendBulkEmails::class, function ($job) {
return $job->recipientCount === 100;
});
}
public function test_job_pushed_to_correct_queue()
{
Queue::fake();
$this->post('/admin/send-newsletter', [/* ... */]);
Queue::assertPushedOn('emails', SendBulkEmails::class);
}
public function test_job_not_pushed_when_validation_fails()
{
Queue::fake();
$this->post('/admin/send-newsletter', [
'subject' => '', // Invalid
]);
Queue::assertNotPushed(SendBulkEmails::class);
}
}
Testing Queued Notifications
Combine notification and queue testing for queued notifications:
<?php
public function test_notification_is_queued()
{
Notification::fake();
Queue::fake();
$user = User::factory()->create();
// Send notification (should be queued)
$user->notify((new InvoiceGenerated())->onQueue('notifications'));
// Assert notification was queued, not sent immediately
Notification::assertSentTo($user, InvoiceGenerated::class);
// Can also verify queue properties
Queue::assertPushed(function ($job) {
return $job->queue === 'notifications';
});
}
Testing Broadcast Events
For real-time features using broadcasting (WebSockets, Pusher, etc.), use the Event facade:
<?php
namespace Tests\Feature;
use App\Events\MessageSent;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class ChatTest extends TestCase
{
use RefreshDatabase;
public function test_message_broadcast_on_send()
{
Event::fake([MessageSent::class]);
$user = User::factory()->create();
$recipient = User::factory()->create();
$this->actingAs($user)
->post('/messages', [
'recipient_id' => $recipient->id,
'body' => 'Hello!',
]);
// Assert event was broadcasted
Event::assertDispatched(MessageSent::class, function ($event) use ($recipient) {
return $event->message->recipient_id === $recipient->id &&
$event->message->body === 'Hello!';
});
}
public function test_broadcast_to_correct_channel()
{
$user = User::factory()->create();
$message = Message::factory()->create();
$event = new MessageSent($message);
// Assert broadcast channel
$this->assertEquals(
'user.'.$user->id,
$event->broadcastOn()->name
);
}
}
Testing Email Attachments
Verify that emails include the correct attachments:
<?php
public function test_invoice_email_has_pdf_attachment()
{
Mail::fake();
$order = Order::factory()->create();
// Send invoice email
Mail::to('customer@example.com')->send(new InvoiceMail($order));
Mail::assertSent(InvoiceMail::class, function ($mail) {
$attachments = $mail->attachments;
return count($attachments) === 1 &&
$attachments[0]['options']['mime'] === 'application/pdf' &&
str_contains($attachments[0]['file'], 'invoice.pdf');
});
}
public function test_report_email_has_multiple_attachments()
{
Mail::fake();
$this->post('/reports/send', [
'type' => 'monthly',
'attachments' => ['sales.csv', 'inventory.xlsx'],
]);
Mail::assertSent(ReportMail::class, function ($mail) {
return count($mail->attachments) === 2;
});
}
Testing Notification Database Storage
When using the database notification channel, test that notifications are properly stored:
<?php
public function test_notification_stored_in_database()
{
$user = User::factory()->create();
$user->notify(new NewFollower($follower));
// Assert notification exists in database
$this->assertDatabaseHas('notifications', [
'notifiable_id' => $user->id,
'notifiable_type' => User::class,
'type' => NewFollower::class,
]);
// Verify notification data
$notification = $user->notifications->first();
$this->assertEquals($follower->id, $notification->data['follower_id']);
$this->assertNull($notification->read_at);
}
public function test_marking_notification_as_read()
{
$user = User::factory()->create();
$user->notify(new NewFollower($follower));
$notification = $user->unreadNotifications->first();
$notification->markAsRead();
$this->assertDatabaseHas('notifications', [
'id' => $notification->id,
]);
$this->assertNotNull($notification->fresh()->read_at);
}
Best Practice: Test notification logic separately from delivery mechanisms. Test that the right notification is created with correct data, then test that it's delivered via the expected channels.
Testing with Real Queue Workers
Sometimes you need to test the actual job execution, not just dispatch:
<?php
public function test_email_sent_after_job_processes()
{
// Don't fake queue, but fake mail
Mail::fake();
$user = User::factory()->create();
// Dispatch job synchronously
dispatch_sync(new SendWelcomeEmail($user));
// Assert email was actually sent
Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
}
Testing Failed Notification Scenarios
Test what happens when notifications fail:
<?php
public function test_handles_invalid_email_address()
{
Mail::fake();
$user = User::factory()->create([
'email' => 'invalid-email', // Invalid
]);
try {
$user->notify(new AccountVerified());
$this->fail('Expected validation exception');
} catch (\Exception $e) {
// Expected behavior
$this->assertStringContainsString('invalid', $e->getMessage());
}
Mail::assertNotSent(AccountVerified::class);
}
public function test_notification_retries_on_failure()
{
Queue::fake();
$user = User::factory()->create();
// Create a job that should retry
dispatch(new SendCriticalAlert($user))->onQueue('high');
Queue::assertPushed(SendCriticalAlert::class, function ($job) {
// Verify retry configuration
return $job->tries === 3 &&
$job->backoff === 60;
});
}
Warning: Be careful with assertion counts. If your code sends multiple emails or notifications, assertSent() without a count parameter will pass as long as at least one was sent.
Testing Email Localization
Test that emails are sent in the correct language:
<?php
public function test_email_sent_in_user_preferred_language()
{
Mail::fake();
$user = User::factory()->create([
'locale' => 'es', // Spanish
]);
$user->notify(new OrderShipped($order));
Mail::assertSent(OrderShipped::class, function ($mail) use ($user) {
$mailData = $mail->toMail($user);
// Check that subject is in Spanish
return $mailData->subject === 'Pedido Enviado' &&
str_contains($mail->render(), 'Su pedido ha sido enviado');
});
}
Testing Broadcast Presence Channels
For presence channels, test authorization:
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PresenceChannelTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_join_chat_room_presence_channel()
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post('/broadcasting/auth', [
'channel_name' => 'presence-chat-room.1',
]);
$response->assertOk();
$response->assertJson([
'channel_data' => [
'user_id' => $user->id,
'user_info' => [
'name' => $user->name,
],
],
]);
}
public function test_unauthorized_user_cannot_join_private_room()
{
$user = User::factory()->create();
$privateRoom = Room::factory()->private()->create();
$response = $this->actingAs($user)
->post('/broadcasting/auth', [
'channel_name' => "presence-chat-room.{$privateRoom->id}",
]);
$response->assertForbidden();
}
}
Exercise 1: Create a test suite for a password reset email that verifies: (1) email is sent to correct user, (2) reset token is included in email, (3) reset link is valid, (4) email expires after 60 minutes, and (5) email is localized based on user preference.
Exercise 2: Write tests for a multi-channel notification (email + SMS + push) that verifies each channel receives the correct message format and that fallback channels are used when primary channels fail.
Exercise 3: Test a queued email campaign system that sends different emails based on user segments, ensuring: (1) jobs are batched correctly, (2) rate limiting is respected, (3) failed emails are retried, and (4) campaign metrics are tracked.
Summary
In this lesson, we've covered comprehensive testing strategies for email and notification systems:
- Using Mail and Notification fakes to prevent actual sends during tests
- Testing mailable content, attachments, and recipients
- Verifying notification channels and delivery methods
- Testing queued emails and notifications with Queue fakes
- Testing broadcast events and presence channels
- Testing database-stored notifications
- Handling failed notifications and retry logic
- Testing email localization and personalization
These testing techniques ensure your application's communication systems work reliably without sending test messages to real users or flooding external services during automated testing.