How to test mail sent from Notification in Laravel

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company

Mastering Mail Testing in Laravel: How to Verify Notifications Without Sending Real Emails

As developers working with modern application stacks like Laravel, testing asynchronous operations—especially those involving external services like email delivery—is a crucial and often tricky part of the development lifecycle. When you introduce custom notification logic, as seen in your scenario, you move beyond simple CRUD operations and into testing the entire communication pipeline.

You’ve encountered a classic hurdle: using Mail::fake() and Mail::assertSent() works seamlessly when sending actual Mailable classes, but fails when dealing with custom notification objects that generate raw MailMessage instances internally. This post will dive into why this happens and provide robust strategies for testing your notification system effectively within the Laravel framework.

The Root of the Problem: Mailable vs. MailMessage in Testing

The core issue lies in how Laravel's testing utilities interact with mail delivery. For Mail::fake() to track sent emails, it expects the object being sent to be a concrete implementation of Mailable. When your custom notification method (toMail()) returns a raw Illuminate\Mail\Message object instead of a Mailable, the fake mechanism cannot properly intercept or assert the delivery event.

This forces you into a dilemma: either refactor your notification structure to adhere strictly to Laravel's mailables, or find a testing approach that bypasses this strict requirement.

Strategy 1: The Recommended Path – Adhering to Mailable Contracts

The cleanest and most idiomatic solution within the Laravel ecosystem is to ensure that anything intended to be sent via the Mail facade inherits from Mailable. This allows you to leverage Laravel's built-in testing tools directly, making your tests more readable and maintainable.

If your goal is strictly email delivery, your notification should encapsulate the mail content within a Mailable class.

Refactoring Example: Instead of having TestNotification generate the message directly, it should generate a concrete Mailable that contains the necessary data.

// Refactored approach: Creating a proper Mailable for testing
class NotificationMailable extends Mailable
{
    use Queueable;

    public $subject;
    public $body;

    public function __construct(string $subject, string $body)
    {
        $this->subject = $subject;
        $this->body = $body;
    }

    public function build()
    {
        return $this->subject($this->body);
    }
}

// Updated Notification logic (simplified for testing context)
class TestNotification extends Notification
{
    // ... other methods

    public function toMail($notifiable): Mailable
    {
        // Return a proper Mailable object
        return new NotificationMailable(
            $this->email['subject'],
            'Testing Notifications!' // Construct the actual mail content here
        );
    }
}

With this refactoring, your original test becomes much cleaner and relies on Laravel’s intended testing flow:

public function test_instant_notification_is_sent(): void
{
    Mail::fake();

    // ... setup user data ...

    $this->user->notify(new TestNotification($emailData, $smsData, $database));

    // Now assertion works correctly because we returned a Mailable
    Mail::assertSent(NotificationMailable::class); 
}

This approach aligns perfectly with the principles of SOLID design and keeps your code compatible with broader Laravel features. For deeper insights into object-oriented patterns in PHP, understanding interfaces and abstract classes is key—something emphasized heavily by frameworks like those powering https://laravelcompany.com.

Strategy 2: Testing Custom Logic Without Mailable (The Workaround)

If refactoring the entire notification structure is currently infeasible (perhaps due to legacy code or strict architectural constraints), you can test the logic of your custom notification without relying on the full Mail facade mocking. This involves using mocks or spies to verify that your notification method was called correctly and that it produced the expected payload.

Instead of asserting against Mail::sent(), you assert against the data transformation:

  1. Mock the Dependency: If your notification relies on another service (e.g., a dedicated Mailer class), mock that service.
  2. Verify Payload Generation: Assert that the method responsible for composing the mail content generated the correct string or data structure, regardless of whether it's packaged as a Mailable.

This strategy shifts the focus from testing the delivery mechanism (which we are bypassing) to testing the business logic within your notification class itself. It ensures that when notify() is called, the internal state changes correctly and the necessary data is prepared for whatever downstream service will handle the actual sending.

Conclusion

Testing external interactions in Laravel requires thoughtful design. While it is tempting to use mocking tools like Mail::fake(), understanding the contract between your custom code and Laravel's framework components is paramount. For robust testing, strive to adhere to established patterns—like using Mailable classes—as this provides the most stable foundation for your tests. By choosing the right abstraction level, you ensure that your test suite accurately reflects both your application's behavior and its intended flow, making development much smoother, in line with best practices promoted by https://laravelcompany.com.