Swift Mailer, Symfony and spooling emails for testing purposes

Author: Matt
Thursday, October 27 2016

We've found ourselves needing some higher-level tests for some emails that were being sent recently (via a command that's run regularly from a cron) - here's how we did it!

We wanted to check that the email content and attachments were correct once we'd run the command.

We could have had the emails sending, and be caught by something like Mailcatcher - but that wasn't a route we wanted to go down. We didn't need to actually check that emails are fully sending via our email provider.

Instead, we configured Swift Mailer to spool emails to a file (these are ignored from git of course) in our test environment.

Then within our tests, we can check the file on disk for the correct contents and attachments. This happens within Behat contexts currently.

These tests aren't particularly fast (so you don't want many of them, just the critical path), and there'd be lower-level ways of doing this, which we'll be refactoring to at some point.

This relies on the Behat Symfony2 Extension

app/config/config_test.yml

swiftmailer:
    disable_delivery: true
    spool:
        type: file
        path: '%kernel.root_dir%/spool'
    delivery_addresses: ~

Then we have an EmailContext that handles a @BeforeScenario hook:

<?php

namespace Vivait\Context;

use Behat\Behat\Context\Context;
use Behat\Symfony2Extension\Context\KernelDictionary;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpFoundation\File\File;

class EmailContext implements Context
{

    use KernelDictionary;

    /**
     * We need to purge the spool between each scenario
     *
     * @BeforeScenario @clear-emails
     */
    public function purgeSpool()
    {
        $filesystem = new Filesystem();
        $finder = $this->getSpooledEmails();

        /** @var File $file */
        foreach ($finder as $file) {
            $filesystem->remove($file->getRealPath());
        }
    }

    /**
     * @return Finder
     */
    public function getSpooledEmails()
    {
        $finder = new Finder();
        $spoolDir = $this->getSpoolDir();
        $finder->files()->in($spoolDir);

        return $finder;
    }

    /**
     * @param $file
     *
     * @return string
     */
    public function getEmailContent($file)
    {
        return unserialize(file_get_contents($file));
    }

    /**
     * @return string
     */
    protected function getSpoolDir()
    {
        return $this->getContainer()->getParameter('swiftmailer.spool.default.file.path');
    }
}

This has a @BeforeScenario hook that looks for @clear-emails on a scenario. If so, it'll clear out the files from any previously sent emails in tests so we're working with a clean directory.

Now if we need to check any emails within that directory, we can loop through them in contexts and check whatever we may need to:

$files = $this->emailContext->getSpooledEmails();
$found = false;

/** @var File $file */
foreach ($files as $file) {
    /** @var Swift_Message $data */
    $data = $this->emailContext->getEmailContent($file);
    /*
     * Check whatever we may need to check with $data
     * You can check data, contents, subject, attachments, addresses etc
     */
    $found = true;
}

Assert::assertNotFalse($found, 'No emails found.');

There's lots of other ways to do this, but this is what we've settled on currently.

For information on how to access other contexts in Behat 3 (like you see here with $this->emailContext) see this gist from stof or the page on the behat documentation

Controlling whitespace in Twig Ansible and Let's Encrypt for Multi-Tenancy Applications