Testing & TDD

Testing Email & Notifications

25 min Lesson 21 of 35

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.