Unit Testing Email Functionality in Zend Framework Applications

Testing applications that send out emails could be quite a hassle if the tools provided by the framework are not properly leveraged. Zend Framework is quite a test-friendly suite and, as you will soon see, it provides the necessary components to help with the testing process. My requirements for the tests are:

  1. They should be repeatable and consistent
  2. There should be no need to change production code to enable testing

The framework tools that I rely on to help with email testing (using Zend_Mail) are

  1. Zend_Mail_Transport_File
  2. Zend_Mail_Message_File

The logic used is simple. During testing, intercept the default mail transport and replace it with Zend_Mail_Transport_File. This component causes email to be written as a file to the directory location provided. Thereafter, read back the file, parse it into its components using Zend_Mail_Message_File, and finally assert that all parts of the email tally with the expected result.

The tests that suffice for my purposes are:

  • Test that the email was sent
  • Test subject
  • Test body text
  • Test recipients

Sample code to achieve all the above is detailed below:

1. Create a new Zend Framework project.

2. Edit the .htaccess file and add the following line at the top :

SetEnv APPLICATION_ENV “development”

We will use this environment variable as a cue to determine if we are in the production phase or test/dev phase.

3. Create a folder named ‘sentmail’ inside the ‘application’ folder. This is where email files generated by the tests will be stored.

4. Edit the Bootstrap.php file and add the function _initMailTransport(); the function _initMailTransport() is responsible for changing the Mail Transport to Zend_Mail_Transport_File. The ‘callback’ parameter is a function that returns the name you want to assign to the file that is generated. The names generated by default are random and testing the order/count/sequence of multiple emails becomes difficult. I therefore choose to provide my own file naming convention.

Note: If you are certain that only a single email is ever sent on a request cycle, then you can use the default file name and avoid the ‘callback’ parameter.

<?php
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    protected function _initMailTransport()
    {
        //change the mail transport only if dev or test
        if (APPLICATION_ENV <> 'production') {
            $callback = function()
            {
                return 'ZendMail_' . microtime(true) .'.tmp';
            };
            $fileTransport = new Zend_Mail_Transport_File(
                array('path' => APPLICATION_PATH . '/sentmail',
                    'callback'=>$callback)
            );
            Zend_Mail::setDefaultTransport($fileTransport);
        }
    }

}

The custom callback function uses the ‘microtime()’ function to generate the file name. We retrieve the emails sorted by their name. This convention ensures that we extract our emails in the sequence in which they are dispatched.

Note: I tried using the php functions filemtime() and filectime() to return the create/modify date of the files to achieve the same effect, but was unsuccessful. Both these functions return time with the least granularity of seconds, making it impossible to reliably sequence multiple emails.

5. Here is the Index controller/Index action.. The index action shoots off two emails using Zend_Mail:

<?php

class IndexController extends Zend_Controller_Action
{

    public function init()
    {
        /* Initialize action controller here */
    }

    public function indexAction()
    {
        $firstMail = new Zend_Mail();
        $firstMail->setFrom('me@yahoo.com');
        $firstMail->addTo('recipient1@yahoo.com');
        $firstMail->addCc('recipient2@yahoo.com');
        $firstMail->setSubject('This is first test email');
        $firstMail->setBodyHtml('Email body goes here!');
        $firstMail->send();

        $secondMail = new Zend_Mail();
        $secondMail->setFrom('me@yahoo.com');
        $secondMail->addTo('recipient3@yahoo.com');
        $secondMail->addCc('recipient4@yahoo.com');
        $secondMail->setSubject('This is second test email');
        $secondMail->setBodyHtml('2nd email');
        $secondMail->send();
    }

}

6. And finally, the test class file (IndexControllerTest.php) is shown below:

<?php

class IndexControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{

    private $params;
    private $urlParams;
    private $pageUrl;

    public static function setUpBeforeClass()
    {
        self::clearFiles();
        parent::setUpBeforeClass();
    }

    public function setUp()
    {
        $this->bootstrap = new Zend_Application(APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini');
        parent::setUp();

        $this->params = array('action' => 'index', 'controller' => 'Index', 'module' => 'default');
        $this->urlParams = $this->urlizeOptions($this->params);
        $this->pageUrl = $this->url($this->urlParams);
        $this->dispatch($this->pageUrl);
    }

    public function testIndexAction()
    {
        // assertions
        $this->assertModule($this->urlParams['module']);
        $this->assertController($this->urlParams['controller']);
        $this->assertAction($this->urlParams['action']);
        $this->assertQueryContentContains("div#welcome h3", "This is your project's main page");
    }

    public function testTwoEmailsAreSent()
    {
        $fileMails = $this->getEmails();
        $this->assertEquals(2, count($fileMails));
    }

    public function testFirstEmailIsAccurate()
    {
        //assertions
        $fileMail = $this->getEmails();
        $this->assertContains('me@yahoo.com', $fileMail[0]->getHeader('from'));
        $this->assertContains('recipient1@yahoo.com', $fileMail[0]->getHeader('to'));
        $this->assertContains('recipient2@yahoo.com', $fileMail[0]->getHeader('cc'));
        $this->assertContains('This is first test email', $fileMail[0]->getHeader('subject'));
        $this->assertContains('Email body goes here', $fileMail[0]->getContent());
    }

    public function testSecondEmailIsAccurate()
    {
        //assertions
        $fileMail = $this->getEmails();
        $this->assertContains('me@yahoo.com', $fileMail[1]->getHeader('from'));
        $this->assertContains('recipient3@yahoo.com', $fileMail[1]->getHeader('to'));
        $this->assertContains('recipient4@yahoo.com', $fileMail[1]->getHeader('cc'));
        $this->assertContains('This is second test email', $fileMail[1]->getHeader('subject'));
        $this->assertContains('2nd email', $fileMail[1]->getContent());
    }

    protected function tearDown()
    {
        self::clearFiles();
        parent::tearDown();
    }
    /**
     *helper function that retrieves emails generated in the folder
     * @return \Zend_Mail_Message_File 
     */
    private function getEmails()
    {
        $directory = APPLICATION_PATH . '/sentmail';
        //remove the pesky .. and .
        $files = array_diff(scandir($directory), array('..', '.'));
        sort($files);  //IMPORTANT - We need them in order!
        $emails = array();
        foreach ($files as $file) {
            $email_str = APPLICATION_PATH . '/sentmail/' . $file;
            $emails[] = new Zend_Mail_Message_File(array('file' => $email_str));
        }
        return $emails;
    }
    /**
     *Internal function to delete all emails created in folder.
     * Need each test case to start clean 
     * It is static because it is also called from "setUpBeforeClass"
     */
    private static function clearFiles()
    {
        //delete all files in folder
        $directory = APPLICATION_PATH . '/sentmail';
        $files1 = array_diff(scandir($directory), array('..', '.'));
        foreach ($files1 as $val) {
            unlink($directory . "/" . $val);
        }
    }

}

The “Setup()” function (called before every test) invokes the index action in the index controller. The getEmails() function opens the /sentmail folder, iterates through every file, parses them using the Zend_Mail_Message_File class, stores the results into an array, and finally returns the entire array to the calling routine. This way, $emails[0] will always be the first email message sent, $emails[1] will be the second email sent and so on..

The clearFiles() method basically blows away the contents of the /sentmail folder. This is invoked on teardown() (after every unit test). I also call this from the setupBeforeClass() method to make absolutely sure that there are no files in the /sentmail folder at startup (for instance, from opening the application index page on the browser).

And finally, the testrunner output generated by these tests is shown below:

image

 

Once you are done with testing, simply edit the .htaccess file and change the environment to “production” like so:

SetEnv APPLICATION_ENV “production”

This will cause the bootstrap to skip the code in the _initMailTransport() function, and normal email service will resume!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s