Unit test Laravel pre-5.3 Mail facade

 Reading time ~4 minutes

Heads up: this article is over a year old. Some information might be out of date, as I don't always update older articles.

Introduction

This post is only serving as a reference to how the Mail facade should be unit tested in Laravel versions previous to 5.3.


When testing an application we don’t want to send actual emails to users. We just want to fake them so we can ensure that they are getting sent, at some point, to the right users and with the right content.

Laravel provides Mail Fakes since version 5.3. This functionality allows you to easily mock the Mail facade and make various assertions on it. The following snippet, for example, comes from the Laravel documentation:

<?php

// Fake Mail functionality to prevent mail from being sent
Mail::fake();

// Assert that a mailable was sent
Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
    return $mail->order->id === $order->id;
});

// Assert a message was sent to the given users...
Mail::assertSentTo([$user], OrderShipped::class);

This syntax however is not available for older versions of Laravel.

Mail Mocking for old Laravel 5 versions

Notice: remember that we are not limited to Mail, but we can use the same mocking functionality on every facade provided by Laravel.

Laravel versions previous to 5.3 don’t have the fake method on facades (Mail, Notification, etc.) and the assertions listed above. However if you’re still using Laravel 5.1 or 5.2 you can mock the calls to the facade anyway, since the shouldReceive method will return an instance of a Mockery mock.

Making assertions on it is a little bit trickier, but still totally possible. For example consider this code that sends a notification via mail when the download of a file is available:

<?php

Mail::send('emails.notification', compact('file'), function ($message)
{
    $message->to('user@foobar.com', 'Foo Bar')
        ->subject('Your download is available');
});

We know from the documentation that we can unit test the send method of the facade using the following syntax:

<?php

Mail::shouldReceive('send')->once();

But how can we make assertions on the view or on the recepient of the message? Fear no more:

<?php

Mail::shouldReceive('send')
    ->once()
    ->with(
        Mockery::type('string'), // assert that the email template is a string
        Mockery::type('array'),  // assert that view data is an array
        Mockery::on(function (Closure $closure)
        {
            $mock = Mockery::mock(\Illuminate\Mail\Message::class);
            $mock->shouldReceive('to')->once()->with('user@foobar.com', Mockery::type('string'))->andReturn($mock);
            $mock->shouldReceive('subject')->once()->with(Mockery::type('string'));

            $closure($mock);

            return true;
        })
    );

The previous code allows us to make different assertions on the parameters of the send function. As you can see, we also have to recursively mock the $message variable, which is an instance of Illuminate\Mail\Message, in order to make assertions on it. Furthermore the assertions are not so tight because we are interested only in testing the relevant parts of the functionality (for example we don’t want to break tests if the subject of the email changes), but you can adjust them to your desired precision.

Other options

There are also other options for testing emails that are worth considering:

Array driver

PHPUnit can change environment variables during testing, so we can swap the Mail driver with the built-in array driver in order to catch the messages. This approach is really simple and straightforward.

First, make sure that the application takes the mail driver from the environment variables in config/mail.php.

<?php

return [
    // ...
    'driver' => env('MAIL_DRIVER', 'smtp'),
];

Add the environment variable to the PHPUnit configuration (the phpunit.xml file), or use a dedicated .env file:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit ...>
    ...
    <php>
        ...
        <env name="MAIL_DRIVER" value="array"/>
    </php>
</phpunit>

Finally you can get the sent messages in your tests and make assertions on them:

<?php

$emails = $this->app->make('swift.transport')->driver()->messages();

$this->assertCount($emails, 1);
$this->assertEquals(['user@foobar.com'], array_keys($emails[0]->getTo()));

Mailthief package

The developers from Tighten kindly released a fake mailer for testing emails that makes the whole process a breeze. It is compatible with all Laravel 5.* versions and provides APIs to make assertions with a clean and concise syntax. Let’s see an example modeled on our previous code:

<?php

// Provides convenient testing traits and initializes MailThief
use InteractsWithMail;

public function test_user_should_receive_notification_email_when_file_is_available_for_download()
{
    // ...

    // Check that an email was sent to this email address
    $this->seeMessageFor('user@foobar.com');

    // Make sure the email has the correct subject
    $this->seeMessageWithSubject('Your download is available');

    // Make sure the email was sent from the correct address
    $this->seeMessageFrom('noreply@acme.inc');

    // Make sure the header is set to a given value
    $this->seeHeaders('X-MailThief-Variables', 'mailthief');

    // Make sure the email contains text in the body of the message
    // Default is to search the html rendered view
    $this->assertTrue($this->lastMessage()->contains('Some text in the message'));
}

That’s all for now!

comments powered by Disqus

Use import aliases in Rollup module bundler

Introduction

Sometimes, when working with JavaScript and module bundlers, it’s useful to create aliases in order to …