Donnerstag, 13. September 2012

symfony2: Testing email sending

On my endless struggle to achieving 100% code coverage and catching every possible (and impossible) test case (I'm just kiddinh, please don't do this!) I was in need for some way to test the sending of emails. This blog post will show you how to test this within a symfony2 (I'm using symfony 2.1 RC2) application, using the default swiftmailer.

Let's be clear hear: I'm not talking about unit tests. These have to be done in swiftmailer or the symfony swiftmailer bundler. I'm talking about functional tests of your application where you want to check if a particular action by your user (or your admin, or your cronjob, or whatever triggers the sending of an email) does lead to an email being sent.

Theory first

When doing functional or integration tests, you don't want to mock stuff because the actual class or library might behave different. Mocking should be done when doing unit tests. Normally you can check the outcome of the whole software being executed pretty nice, for example when you use the symfony test client to request a web page and then check the result. Doing the same with emails is a bit more complicated.

One way would be to sent the email, then connect to the mailbox (through pop3 or imap), parse the email and see if everything worked. Doing this will bring up a few problems:
  • You rely on external factors (like email servers) to work very fast and very reliable. A mailserver which takes 5 seconds to deliver the email might cause your test to fail or get very slow.
  • This causes your tests to end up failing in different ways when the part of your software you can control actually works.
  • Depending on how often you do this tests and how many emails you need to check, you have much overhead both in email volume and the time it takes to execute the tests.
So what you want to actually check is if the swiftmailer passes the email to the smtp. Using an smtp dummy you could sent the email without the need to pass it to an real email server, but again, this means much overhead.

I opted for another solution: I check if the correct events are triggered when sending the email. If swiftmailer works (and the settings of the smtp are correct) this means the email got to the smtp.

A dummy listener for testing

Swiftmailer ships with a event dispatching system. It calls this listeners "Plugins" and the Swift_Mailer class has a method called registerPlugin() which accepts a Swift_Events_EventListener parameter. By creating a dummy listener which stores relevant data and registering it we can add checks for this data later to see if everything worked as expected.

First, let's create the dummy Listener:

class EmailListener implements \Swift_Events_SendListener
{

    public $beforeSendEvt = null;
    public $sendEvt = null;

    public function beforeSendPerformed(\Swift_Events_SendEvent $evt)
    {
        $this->beforeSendEvt = $evt;
    }

    public function sendPerformed(\Swift_Events_SendEvent $evt)
    {
        $this->sendEvt = $evt;
    }
}


Ok, so we got ourself a listener which implements the both methods needed and just stores the events it recieves.We store it in public properties so we can access it in our tests. This is not clean code, but as this is a test dummy I tend to keep it simple. If I add more functionality, I might change this later.

Plugging it all together

Now let's see if the email really gets sent:

class EmailTest extends WebTestCase
{

    /**
     * @test
     */
    public function testEmail()
    {
        $container = static::$kernel->getContainer();
        $mailer = $container->get('mailer');

        $plugin = new EmailListener();
        $mailer->registerPlugin($plugin);


        // Do your stuff here which triggers the swiftmailer to sent an email

        $this->assertNotNull($plugin->beforeSendEvt);
        $this->assertNotNull($plugin->sendEvt);
    }
}


Looks pretty straightforward: Get the mailer out of the container, register your listener, then do whatever is needed to sent the email and check if both properties of the listener are filled. Of course you can do more in-depth checks on the events stored to see if the email is correct (checking the sender, reciever, subject and so on).

There's always more

Of course you can extend this test setup. I believe my way is a pretty easy setup and works very well if you have a simple SMTP setup which doesn't change much. If you also want to check if swiftmailer and your configuration are correct you could introduce one test which sents an email through a dummy SMTP or the real SMTP while you rely on the events for all other emails. This way you keep your external dependencies to a minimum but also get some feedback on if your emails are being sent.

Improving the tests may involve checking the event for specific stuff. You can extract the actual message sent through $plugin->sendEvt->getMessage(). With this message you can check for sender, recipient, other message header stuff or the body.

I hope this little tutorial helps you catching email errors within your code before they get into production. If you got improvements I would be happy to hear from you (comments, email, twitter).

UPDATE: If you are using behat, you can test for email's with build in functionality (thanks to @biruwon for sending me the link): http://docs.behat.org/cookbook/using_the_profiler_with_minkbundle.html#implementing-email-step-logic

Keine Kommentare:

Kommentar veröffentlichen