Email Delivery & Assertions in Tests

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We just got our registration functional test to pass. But to do it, we had to configure the test environment with our Mailtrap credentials. And that means that each time we run our tests, an email is actually being delivered to Mailtrap!

Ok, in reality, because we're using Mailtrap... we're not really sending test emails to real people. But delivering emails inside our tests is a bummer for a few reasons: it adds a lot of garbage emails to Mailtrap, it slows down our tests and it means that we need to worry about configuring real Mailtrap credentials just to check if our registration test passes.

The truth is, we don't really need emails to be sent in the test environment. We do want the Email objects to be created and processed by Mailer... but if at the last second Mailer... just... didn't actually deliver them... that would be cool! We could try to do this by, maybe adding an if statement around $this->mailer->send() if we're in the test environment... but that would be a pain... and ugly.

The Null Transport

Way earlier in the tutorial, I mentioned that the way an email is delivered is called a "transport". In .env, we're using the smtp transport to talk to the localhost server. In .env.local, this is also using the smtp transport to talk to the Mailtrap server. So far, smtp is the only transport we've seen.

Well, prepare to be amazed! Introducing the laziest, do-nothing... but mysteriously useful transport ever: the null transport! When you deliver an email via the null transport... your email goes... nowhere.

Hey! That's exactly what we want to do in the test environment! Inside .env.test, change MAILER_DSN to smtp://null.

9 lines .env.test
... lines 1 - 5
MAILER_DSN=smtp://null
... lines 7 - 9

Side note! This syntax changed in Symfony 4.4 to null://default - where the start of the string defines the transport type. We'll talk more about transports in a few minutes when we start using SendGrid.

9 lines .env.test
... lines 1 - 5
MAILER_DSN=smtp://null
# in Symfony 4.4 and higher, the syntax is
# MAILER_DSN=null://default

Anyways, let's try the test now:

php bin/phpunit tests/Controller/SecurityControllerTest.php

It passes and... yea! There were no email sent to Mailtrap. The test also ran about twice as fast.

Using the Null Transport by Default?

But wait, there's more! The null transport is perfect for the test environment. And... it might also be a good candidate as the default transport.

Hear me out. If a new developer cloned this project, they would not have a .env.local file. And so, out-of-the-box, mailer would use the smtp://localhost setting. What if this developer was really a designer that wanted to work on styling the registration process. Well... surprise! The moment they submit the form successfully, they'll be congratulated with a lovely 500 error. And they'll be off to find you to figure out how to fix it. That's no good for anyone.

That's why using the null transport in .env might be a perfect default. Then, if someone actually wants to test how the emails look, then they can take some time to configure their .env.local file to use Mailtrap.

Let's do this: change MAILER_DSN to smtp://null. Use null://default on Symfony 4.4 or higher.

48 lines .env
... lines 1 - 37
###> symfony/mailer ###
MAILER_DSN=smtp://null
# in Symfony 4.4 and higher, the syntax is
# MAILER_DSN=null://default
###
... lines 43 - 48

Over in .env.test, we don't need to override anything. So, remove MAILER_DSN.

Asserting Emails were Sent

We can now use the site and run our tests without needing to manually configure mailer. Cool! But we can still make our functional test a little bit more fun.

In SecurityControllerTest, we are testing that the registration form works. But we are not asserting that an email was in fact sent or... that the email has the right details.

And, while that might not be a huge deal, we can add these types of assertions. Well, actually I can't add them... because this project uses Symfony 4.3. Symfony 4.4 adds a number of new features that make this a pleasure.

Google for "Symfony 4.4 mailer testing" to find a blog post about this fancy new stuff. It's... just... awesome. The setup is the same, but after each request, you can choose from a bunch of assertions to check that the correct number of emails were sent, that it was sent to the right person, the subject... anything!

In our test class, after submitting the form, I'll paste in some assertions that I will use... once I upgrade this app to Symfony 4.4. This checks that one email was sent and then fetches the Email object itself, which you can then use to make sure any part of it is correct.

I'll comment these out for now.

... lines 1 - 6
class SecurityControllerTest extends WebTestCase
{
public function testRegister()
{
... lines 11 - 23
$this->assertResponseRedirects();
/* Symfony 4.4:
$this->assertEmailCount(1);
$email = $this->getMailerMessage(0);
$this->assertEmailHeaderSame($email, 'To', 'fabien@symfony.com');
*/
}
}

Next, it's time to send some real emails people! It's time to get ready for production! Let's register with a cloud email sender and get it working in our app. We're also going to learn more about Mailer's "transport" system.

Leave a comment!

This tutorial is built on Symfony 4.3, but will work well with Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.110.11
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-snappy-bundle": "^1.6", // v1.6.0
        "knplabs/knp-time-bundle": "^1.8", // v1.9.1
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.23
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "league/html-to-markdown": "^4.8", // 4.8.2
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.1,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.1.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.4.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.3.4
        "symfony/console": "^4.0", // v4.3.4
        "symfony/flex": "^1.9", // v1.9.10
        "symfony/form": "^4.0", // v4.3.4
        "symfony/framework-bundle": "^4.0", // v4.3.4
        "symfony/mailer": "4.3.*", // v4.3.4
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.3.4
        "symfony/sendgrid-mailer": "4.3.*", // v4.3.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.3.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "^4.0", // v4.3.4
        "symfony/web-server-bundle": "^4.0", // v4.3.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.6.2
        "symfony/yaml": "^4.0", // v4.3.4
        "twig/cssinliner-extra": "^2.12", // v2.12.0
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.0
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.2.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/browser-kit": "4.3.*", // v4.3.5
        "symfony/debug-bundle": "^3.3|^4.0", // v4.3.4
        "symfony/dotenv": "^4.0", // v4.3.4
        "symfony/maker-bundle": "^1.0", // v1.13.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.3.4
    }
}