Getting Started with Zend_Test

I worked on a project recently where we used Zend Framework. As part of that project, I was tasked with writing unit tests. So, I went to the "tests" directory generated for me by the zf CLI utility to get started. What I found there was three files.

  • application/bootstrap.php
  • library/bootstrap.php
  • phpunit.xml

They were all completely empty, which didn't really provide much in the way of guidance on how to get started. The Zend_Test documentation is good, but was a bit lacking in that area as well; it really only covers how Zend_Test extends the capabilities of PHPUnit.

PHPUnit Configuration

After digging around a bit more, I came across this blog post by Matthew Weier O'Phinney, which gave me most of what I needed. It starts out detailing how to populate the phpunit.xml file. Coupling that advice with a bit more from this webcast from Zendcasts, I came up with the following result.


    
        ./
    

    
        
            ../application/
            
                ../application/
            
        
    

    
        
        
    

Note that I'm only testing code in the "application" directory. If you've taken up the practice of storing some code in a custom vendor subdirectory within the "library" directory of your project, you would want to add a similar set of <directory> and <exclude> elements for that so it's included in the code coverage reports as well.

Bootstrap File

The other part that may be specific to your project is the "bootstrap" attribute of the root "phpunit" element, which holds the path to a file. This file is similar in nature to the bootstrap file (also commonly called the "dispatch script" and not to be confused with the file containing the bootstrap class) that lives in the document root of your project and provides access to your application.

Instead of setting up to service an application request, however, it instead sets up an environment suitable for executing your unit tests. By referencing it in this "bootstrap" attribute, you tell PHPUnit to include it automatically so you don't have to do so explicitly in each of your test case classes. Matthew provides the source for his and mine is based on that, but with some modifications for my own case.

error_reporting(E_ALL | E_STRICT);

date_default_timezone_set('GMT');

define('APPLICATION_ENV', 'testing');
define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));
define('APPLICATION_CONFIG', APPLICATION_PATH . '/configs/application.ini');

set_include_path(implode(PATH_SEPARATOR, array(
    realpath(APPLICATION_PATH . '/../library'),
    realpath(APPLICATION_PATH . '/../tests'),
    get_include_path()
)));

require_once 'Zend/Loader/Autoloader.php';
Zend_Loader_Autoloader::getInstance();

So, here's a play-by-play of what I'm doing here.

  • Cranking the error reporting level up so any possible errors show up in the test results.
  • Defining a default timezone so that Derick doesn't come after me with a cluebat. You may want to set this at the server level instead.
  • Defining the application environment so the appropriate configuration is used.
  • Defining the path to my "application" directory and configuration file, which Zend_Application will eventually accept when I instantiate it.
  • Adding the "library" and "tests" directories to my include_path so that Zend Framework files and my test bootstrap file are accessible for inclusion.
  • Initializing the Zend Framework autoloader.

Notice one thing I'm not creating here is a Zend_Application instance. I actually did try to go that route early on in the project, but found that it had too many side effects when actually trying to run the tests. It's easier to simply create the Zend_Application instance within your test case class.

Also, unless you have a specific need for the bootstrap.php files created by the zf CLI utility, you can delete those at this point. I find this one bootstrap file gives me everything I need. As far as I know, if you wanted to use different bootstraps for "application" and "library", you'd have to manually include them in each test case file as PHPUnit only provides the option to automatically include a single bootstrap file.

Base Database Test Case

Since the controller makes use of the model, I figured it would be prudent to write tests for the latter first. Under tests/application, I created a "models" directory to mirror the directory structure of the application source code. As with database test cases that use the PHPUnit Database extension, there's some common setup logic involved when using Zend_Test_PHPUnit_DatabaseTestCase as well. As such, I created an abstract base class for my models to extend that looks something like this.

abstract class AbstractModelTest extends Zend_Test_PHPUnit_DatabaseTestCase
{
    /**
     * Database connection
     * @var Zend_Test_PHPUnit_Db_Connection
     */
    protected $_db;

    /**
     * Instance of the model to be tested, instantiated by this class
     * @var object
     */
    protected $_model;

    /** 
     * Name of the model class to be tested, intended to be overridden by 
     * subclasses for use by this class when instantiating the model
     * @var string
     */
    protected $_modelClass;

    /** 
     * Path to the directory for data set fixture files
     * @var string
     */
    protected $_filesDir;

    /** 
     * Initializes the model.
     * @return void
     */
    public function setUp()
    {   
        $this->_filesDir = dirname(__FILE__) . '/_files/' . $this->_modelClass;
        $this->_model = new $this->_modelClass($this->getAdapter());
        parent::setUp();
    }

    /**
     * Implements PHPUnit_Extensions_Database_TestCase::getConnection().
     * @return Zend_Test_PHPUnit_Db_Connection
     */
    protected function getConnection()
    {
        if (empty($this->_db)) {
            $app = new Zend_Application(APPLICATION_ENV, APPLICATION_CONFIG);
            $app->bootstrap();
            $options = $app->getOptions();
            $schema = $options['resources']['db']['params']['dbname'];
            $db = $app->getBootstrap()->getPluginResource('db')->getDbAdapter();
            $this->_db = $this->createZendDbConnection($db, $schema);
        }
        return $this->_db;
    }

    /**
     * Implements PHPUnit_Extensions_Database_TestCase::getDataSet().
     * @return PHPUnit_Extensions_Database_DataSet_IDataSet 
     */
    protected function getDataSet()
    {
        return $this->createXmlDataSet(dirname(__FILE__) . '/_files/seed.xml');
    }
}

Here's a breakdown of what's going on in this class. First, setUp() instantiates the model class (via the $_modelClass property, which subclasses must set) and sets the directory where data set fixture files will be stored — I'll explain those in a moment. I use a central directory to store them, but split them out into subdirectories per model class to keep them organized.

A database test case works by connecting to a database, seeding it with a data set, performing on operation on that data set, and then comparing the database contents with another data set representing the desired result to ensure that the two match. To do all this, we need the database connection and the seed data set. Zend_Test_PHPUnit_DatabaseTestCase, like its base class PHPUnit_Extensions_Database_TestCase, requires you to implement the methods getConnection() and getDataSet() to obtain these.

In getConnection(), I'm checking the $_db class property so that I only initialize it once. When I do so, I'm creating an instance of Zend_Application and calling bootstrap() on it so that all resources are bootstrapped. Note that I'm not calling run() since I don't want to service an application request.

Once that's done, I get the application configuration and extract the name of the database from it; I'll need this to initialize the connection. This assumes that you're using the Db application resource — otherwise, get that information however is appropriate for your application.

Next, I obtain the database adapter instance from the Db application resource. Finally, I pass the adapter and schema name to the createZendDbConnection() method of Zend_Test_PHPUnit_DatabaseTestCase to obtain the instance of Zend_Test_PHPUnit_Db_Connection that will finally be stored in the $_db property and then returned for subsequent calls to getConnection().

Creating a data set is a bit easier: I simply call createXmlDataSet() and pass it a path to the seed file. Since it's not specific to a particular model, I just store the seed file on the same level as the per-model subdirectories.

The createXmlDataSet() method uses the XML format defined by DbUnit, a database extension to the JUnit Java unit testing framework. There's also a createFlatXmlDataSet() method for its Flat XML format. If you don't want to massage your data into either of these, CSV is also supported but a bit more involved as it assumes each file represents a single table. I recently contributed a component to PHPUnit that adds support for the MySQL XML format to its Database extension. This blog post goes into more detail about that.

A potential improvement for this base class would be to retrieve the model autoloader and extracting the model namespace from that so it wouldn't have to be hard-coded into the base class or replicated into each subclass's definition of $_modelClass.

Example Database Test Case

Now that the common logic for database test cases is covered, here's an example of an actual test case class that makes use of it.

require_once dirname(__FILE__) . '/AbstractModelTest.php';

class FooTest extends AbstractModelTest 
{
    /** 
     * Model class specification
     * @var string
     */
    protected $_modelClass = 'Namespace_Model_Foo';

    /**
     * Tests adding a foo with invalid data.
     * @return void
     */
    public function testAddFooWithInvalidData()
    {
        // $fooData = ...
        $this->_model->addFoo($fooData);
        $expected = $this->createXmlDataSet($this->_filesDir . '/afterAddFoo.xml');
        $actual = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
        $actual->addTable('table_name');
        $this->assertDataSetsEqual($expected, $actual);
    }
}

An actual test case class would obviously be a bit longer, but this snippet is enough to show what one looks like. I'm specifying the name of the model class I'm testing in the $_modelClass property so that the setUp() method in the base class will instantiate the appropriate model. After that, I simply start defining test case methods, the names of which must be prefixed with "test" as per PHPUnit's default behavior. In these, I can refer to my initialized model to execute the method being tested.

In the test case method itself, I'm testing a model method to add a record to the database with a predetermined set of row data. I have a data set file afterAddFoo.xml that reflects the expected state of the database following the addition of the new record. The QueryDataSet class, which isn't documented in the PHPUnit manual but easily found in the PHPUnit source code and fairly simple to read from there, allows me to get the contents of the live database table receiving the new record. From there, it's a simple assertion to compare the two data sets for equality.

One feature of PHPUnit that was very useful during this project was data providers, which are methods that return an array of multiple sets of arguments for a test method. When the proper annotation is added to the test method, that method is called once for each set of arguments that the data provider method returns. It's handy for testing methods that insert records with multiple sets of invalid data. The manual does a good job of explaining data providers, so I won't replicate that here.

Controller Test Case

Writing controller test cases was a bit trickier. To start with, depending on what you're testing, you may or may not need to integrate database testing into your controller test case. In my case, I did because I was writing a REST API that returned JSON and the expected response depended on the data. Another difficulty (mainly in its discovery) involved a slight disparity in how the bootstrap is made available to the front controller in test cases versus the actual application that to my knowledge currently remains undocumented. Thanks to Ralph Schindler for helping me to figure that one out. Here's an example controller test case class.

class FooControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
    public function setUp()
    {
        $this->bootstrap = new Zend_Application(APPLICATION_ENV, APPLICATION_CONFIG);

        parent::setUp();

        $this->getFrontController()->setParam('bootstrap', $this->bootstrap->getBootstrap());

        // Set up database testing integration
        $options = $this->bootstrap->getOptions();
        $schema = $options['resources']['db']['params']['dbname'];
        $this->_db = $this->bootstrap->getBootstrap()->getPluginResource('db')->getDbAdapter();
        $connection = new Zend_Test_PHPUnit_Db_Connection($this->_db, $schema);
        $seed = dirname(__FILE__) . '/../models/_files/seed.xml';
        $fixture = new PHPUnit_Extensions_Database_DataSet_XmlDataSet($seed);
        $tester = new Zend_Test_PHPUnit_Db_SimpleTester($connection);
        $tester->setupDatabase($fixture);
    }
}

Off we go again.

  • First, I initialize the $bootstrap property of the class with a Zend_Application instance similarly to how I did it in the base model test case. In this case, I don't have to call its bootstrap() method manually as the call to the setUp() method in the parent class takes care of that.
  • Oddly, one thing setUp() does not handle is injecting the bootstrap into the front controller. You have to do this manually, else you're stuck with a non-functioning test case.
  • Finally, I set up database integration testing. This is pretty similar to the getConnection() method in the base model test case class except that I'm instantiating the connection and data set objects directly, then injecting them into Zend_Test_PHPUnit_Db_SimpleTester to handle seeding the database with the data set.

After this is done, it's pretty much like the reference guide reads: set request paramaters, dispatch the request, assert things about the response. If you need a model, simply instantiate it directly and pass it the $_db property that was initialized in setUp() here. If you need database integration with all your controllers, the above class can easily be tweaked to become an abstract base class that your controller test cases extend.

Conclusion

While it did admittedly take a bit of stumbling and finagling to get up and running, writing unit tests after that point was fairly easy and straightforward. It most definitely contributed to the quality of the end product; I caught several issues that might have taken longer to track down had I not written unit tests that exposed them early on. I hope this blog post has helped to get you started writing tests for your Zend Framework application. Happy testing!

Update: Matthew Weier O'Phinney has committed a fix to SVN for the issue involving the Zend_Application instance not being made available to the base controller test case class. This will be included in the Zend Framework 1.10 release.

Awesome introduction to Zend Framework unit testing with MySQL

Hey Matthew,

Very nice article about unit testing Zend Framework applications, especially with the part of using a MySQL backend.

Althoug this article is similar to my blog article on unit testing zend framework applications (http://www.dragonbe.com/2009/11/unit-testing-with-zend-framework-18.html), I do find it interesting to use the specific Zend_Test_PHPUnit_DatabaseTestCase instead of using a script to access data resources when needed.

I'm definitely going to try out your examples here and work them within my own set up.

Great job and well documentend !

Michelangelo

Very complete, but I would

Very complete, but I would create the main Zend_Application object to allow simple autoloading of various model and form classes.

Good point

I had set up the bootstrap to autoload models, but didn't consider setting them up as a bootstrap resource. How exactly do you handle it? One resource method per model class seems extensive. A configured loader, maybe?

How to include classes?

I am trying to start testing my models, but I am having a problem where none of my models are being included, nor the classes they are extending. So I am getting error messages saying that my Model can't be found. You don't mention any require_once, but even when I call require_once, I am having a hard time requiring all of the classes that are required. Have you encountered this situation? What have you done about it? Is autoloading enabled in this example? What should I do?

Yes, autoloading is enabled

Yes, autoloading is enabled in this example. For Zend Framework files, use its autoloader. For your model classes, you just need to set up a resource autoloader in a resource method of your bootstrap class.