Testing emails with Laravel

Reliability of emails subsystem is essential for current web applications, but testing emails is not as easy at it looks at first glance. Laravel doesn’t provide a good solution for testing emails out of the box, so after a bit of Googling I come up with following solution for testing emails.

Helper

Basic idea is build on attaching event listener to Swift emails package, for this I extended base class Swift_Events_EventListener that takes as parameter our test case.

/**
 * Class TestingMailEventListener
 *
 * @package Tests
 */
class TestingMailEventListener implements Swift_Events_EventListener
{
    protected $test;

    /**
     * TestingMailEventListener constructor.
     *
     * @param $test
     */
    public function __construct($test)
    {
        $this->test = $test;
    }

    /**
     * Before email sent
     * 
     * @param Swift_Events_SendEvent $event
     */
    public function beforeSendPerformed(Swift_Events_SendEvent $event)
    {
        $this->test->addEmail($event->getMessage());
    }
}

Then I created a Trait with following method:

/**
 * Trait TracksEmails
 *
 * @package Tests
 */
trait TracksEmails
{
    /**
     * Delivered emails.
     * @var array
     */
    protected $emails = [];

    /**
     * Register a listener for new emails.
     *
     * @before
     */
    public function setUpMailTracking()
    {
        Mail::getSwiftMailer()
            ->registerPlugin(new TestingMailEventListener($this));
    }
...

Which registers event listener and collects all emails sent with Swift.

Having this done it’s easy to check whether particular email was sent, I also added bunch of helpers to make it easier:

/**
 * Assert that at least one email was sent.
 * @return $this
 */
protected function seeEmailWasSent()

/**
 * Assert that the given number of emails were sent.
 *
 * @param integer $count
 * @return $this
 */
protected function seeEmailsSent($count)

/**
 * Assert that the last email's body contains the given text.
 *
 * @param string $excerpt
 * @param Swift_Mime_Message $message
 * @return $this
 */
protected function seeEmailContains($excerpt, Swift_Mime_Message $message = null)
...

And several more.

Here you can see full helper trait code click.

Usage

Usage of this helper is quite simple, here is simple example:

public function testEmail()
{
    \Mail::raw('Some content', function (Message $message) {
        $message->from('[email protected]', 'Laravel');
        $message->to('[email protected]');
    });

    $this->seeEmailWasSent()
        ->seeEmailTo('[email protected]')
        ->seeEmailContains('Some content');
}

And a bit more complex example of how I test my password reset form:

public function testPasswordReset()
{
    $user = $this->makeUser();

    Event::listen(NotificationSent::class, function (NotificationSent $notification) use ($user) {
        self::$token = $notification->notification->token;
        $this->seeEmailWasSent()
            ->seeEmailTo($user->Login)
            ->seeEmailContains(self::$token);
    });

    $this->visitRoute('password.request')
        ->assertResponseOk()
        ->type($user->Login, 'email')
        ->press('Reset')
        ->assertResponseOk()
        ->see(trans('passwords.sent'))
        ->seeInDatabase('password_resets', ['email' => $user->Login]);

    $this->visitRoute('password.reset', ['token' => self::$token])
        ->assertResponseOk()
        ->type($user->Login, 'email')
        ->type('123123', 'password')
        ->type('123123', 'password_confirmation')
        ->press('Reset Password')
        ->assertResponseOk()
        ->seeRouteIs('dashboard');

    $this->assertTrue($this->isAuthenticated());

    $this->assertTrue(app('hash')->check('123123', $user->fresh()->Password));
}

In this example I used events listener to listen for notification to check if sent token is the correct one.

Currently there are some probably better alternatives for testing emails with Laravel, like this one tightenco/mailthief.