Pages

2009/11/01

Unit testing with Zend Framework 1.8+

I recently gave a presentation at ZendCon 2009 Uncon about "php unit with Zend Framework", which many people liked a lot (see comments on http://joind.in/638). But since then I got a lot of questions how to set up a testing environment for Zend Framework applications that uses version Zend Framework 1.8 or newer.

We start off by setting our environment best fitted for our unit testing. I use a virtual linux system for this, using VMWare, but with some extra background research these global settings can be applied for your own (test) environment as well.

I'm not going to explain how to install PHP here, but I can tell you I'm using PHP 5.3.0 (build from source). With this build, I've installed PHPUnit 3.4.2 and Xdebug 2.0.5. The example application was made using Zend Framework 1.9.5.

First we're creating a new Zend Framework project using Zend_Tool on command line, by issuing the command zf create project zfunit in the workspace directory I use to share my VMWare instance with my workstation.

Ok, we now have a default Zend Framework application running.




We're not going to build a complete application here, but just showing how you can start unit testing your Zend Framework project, so you can implement unit tests on your own applications.

First we need to modify the phpunit.xml file so we can use it as our unit testing configuration.


<phpunit bootstrap="./TestHelper.php" colors="true">
    <testsuite name="Zend Framework Unit Testing">
        <directory>./directory>
    testsuite>

    <filter>
        <whitelist>
            <directory suffix=".php">../library/directory>
            <directory suffix=".php">../application/directory>
            <exclude>
                <directory suffix=".phtml">../application/directory>
            exclude>
        whitelist>
    filter>

    <logging>
        <log type="coverage-html" target="./log/report" charset="UTF-8" yui="true" 
         highlight="true" lowUpperBound="50" highLowerBound="80"/>
        <log type="testdox-html" target="./log/testdox.html" />
    logging>
phpunit>


As you see in the root node of our phpunit.xml file, we include the TestHelper script to aid us setting up the right testing environment. Of course, you can modify this file to fit your own application setup. This is my TestHelper script to set up my test environment, include paths and setting timezones.



// start output buffering
ob_start();

// set our app paths and environments
define('BASE_PATH', realpath(dirname(__FILE__) . '/../'));
define('APPLICATION_PATH', BASE_PATH . '/application');
define('APPLICATION_ENV', 'testing');

// Include path
set_include_path(
    '.'
    . PATH_SEPARATOR . BASE_PATH . '/library'
    . PATH_SEPARATOR . get_include_path()
);

// Set the default timezone !!!
date_default_timezone_set('Europe/Brussels');

// We wanna catch all errors en strict warnings
error_reporting(E_ALL|E_STRICT);

require_once 'ControllerTestCase.php';



We create a custom ControllerTestCase that we can extend in all our controller test cases. This proves to be the most convenient way to handle the setup and teardown of the Zend Framework architecture.



require_once 'Zend/Application.php';
require_once 'Zend/Test/PHPUnit/ControllerTestCase.php';

abstract class ControllerTestCase extends Zend_Test_PHPUnit_ControllerTestCase
{
    public $application;

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

        $this->bootstrap = array($this, 'appBootstrap');
        parent::setUp();
    }

    public function appBootstrap()
    {
        $this->application->bootstrap();
    }
}



And finally we create our first test on the default IndexController.



require_once realpath(dirname(__FILE__) . '/../../ControllerTestCase.php');

class IndexControllerTest extends ControllerTestCase
{
    public function testCallingRootTriggersIndex()
    {
        $this->dispatch('/');
        $this->assertController('index');
        $this->assertAction('index');
    }
  
    public function testCallingBogusTriggersError()
    {
        $this->dispatch('/bogus');
        $this->assertController('error');
        $this->assertAction('error');
        $this->assertResponseCode(404);
    }
}



Running phpunit from commandline will tell you that your assertions succeeded and all is a go.



All green, assertions succeeded and report is generated.

I've setup my test environment that directory log/report is aliased by /report, so I can view the code coverage reports (requires XDebug).
The following code coverage report is the result.


As you can see, with just a minimal effort you've set up your Zend Framework application to be unit tested. So now you don't have any more excuses to leave your applications untested.

Since I don't have all the knowledge, I would like to thank Matthew Weier O' Phinney and Mathias Geat for sharing their knowledge on the subject. Thanks guys !

21 comments:

  1. I left ZendCon knowing what unit testing was, and why I needed to do it, but didn't have any idea how to start... thanks for the "foot in the door" tutorial.

    ReplyDelete
  2. BTW, as of 1.9.5, you can now assign a Zend_Application instance to $this->bootstrap; Zend_Test_PHPUnit_ControllerTestCase will then bootstrap() it during setUp().

    ReplyDelete
  3. Remember that this is how to test your controllers; to test other classes there is no need for the Zend_Test component, plain old phpunit will do a perfect job.

    ReplyDelete
  4. @Lance,

    Glad to help you out here, I'll probably put some more examples on my blog, depending on the amount of questions I get.

    @mweierophinney,

    I did not know that, and you haven't blogged about it yet... but I'll look in to it and will post a follow up once I have figured it out.

    Tnx for the hint !

    @Giorgio,

    Yes, this is solely for testing your controllers, actions and interactions with data sources (internal and external).

    But I have to admit, having this combined with my own library components, I have one testsuite running all tests, independent from their purpose.

    ReplyDelete
  5. Anonymous4/11/09 00:36

    It looks like the xml for phpunit.xml is a bit messed up. No opening < for most of it, maybe other stuff?

    Great post, though. Very helpful!

    Greg

    ReplyDelete
  6. Anonymous4/11/09 00:37

    More on missing code: It looks like the <'s were escaped.

    Thanks again,
    Greg

    ReplyDelete
  7. One thing I found out a couple of months ago: If you have a build server and you are running Hudson, you can generate the code covergae report inside your workspace, bookmark any html page and share it with other members of your team.

    ReplyDelete
  8. Anonymous6/1/10 23:43

    What about Zend Debug? Can you get this to work with it instead of XDebug?

    ReplyDelete
  9. Thanks for your post. Can you please talk about testing models in a 1.8+ application? Thank you! Also, you still have errors in your XML code sample.

    ReplyDelete
  10. @misbehavens: I know, apparently there are a couple of escaping issues here. But any given XML editor will show you immediately what's wrong so you can fix it.

    This example shows more the general idea behind unit testing instead being a simple copy/paste example.

    I'm sorry for the misunderstanding and I'm looking into the whole escaping issue (already pointed out by @ananonymous #1 and @anonymous #2)

    @anynymous #3: XDebug provides extra functionality used by PHPUnit to generate those coverage reports. I'm sure Zend Debug provides similar functionality, but I'm not sure how to implement it along with PHPUnit.

    ReplyDelete
  11. @Federico: do you have published an article of that setup ? I'm really interested in seeing this kind of setup.

    ReplyDelete
  12. Thanks for sharing unit testing so deeply.Can we aplly it to Module testing as well?

    Sweeptakes

    ReplyDelete
  13. This will help me a lot with unit testing! I'm trying to work on the same build source, but am having a little trouble with getting everything to work like I want it to.

    ReplyDelete
  14. @shubhangi

    Modules are nothing more then structured pieces of your application. Working this described way allows you to test your whole application, modules and library components included.

    @c unit testing
    What do you mean with everything ? Have you tried the quickstart guide from Zend Framework ? This setup works well with it.
    See http://framework.zend.com/docs/quickstart for more details

    ReplyDelete
  15. Thanks for giving us more knowledge on unit testing deeply. One question i want to raise here that how to overcome the limitations of the unit testing or can we use unit testing along with integration testing to extent its results quality.

    ReplyDelete
  16. I really like this tutorial. However, one recommendation to prevent out of memory problems:

    Exclude the Zend Framework library from code coverage analysis:

    [...]


    ../library/Custom
    ../application

    ../application



    [...]

    Otherwise the hole Framework will be checked. I wasn't able to get this working, even with memory_limit set to 1024M (my dev environment is hosted on a "small" but reliable system).

    ReplyDelete
  17. I'm just starting out with Zend Framework and Unit Testing so this tut is going to prove useful.
    I have a question about code coverage with XDebug.

    You say "I've setup my test environment that directory log/report is aliased by /report, so I can view the code coverage reports (requires XDebug)."

    Could you provide some pointers on how to do this?

    Cheers
    David [MonkeyPHP]

    ReplyDelete
  18. I wanted to use Phactory to test part of the submit action source as given below: http://awesomescreenshot.com/050a0p2a6

    Using Phactory, it is assured that the data is going into DB but the problem is in code coverage as some of the lines are still highlighted in red.

    How do i test those lines so that the code coverage will be shown in green?

    ReplyDelete
  19. Anonymous27/4/11 18:22

    I followed your example and I'm pretty close to having it working. I'm getting the following error:


    1) IndexControllerTest::testCallingRootTriggersIndex
    Zend_Controller_Exception: No default module defined for this application


    We have the framework separate from the project code to make it easier to update the framework and that caused some file path issues that we believe are resolved, but we are now getting this final error.


    Do you have any possible solutions or things I can check?

    ReplyDelete
  20. @anonymous: if you experience the most discussed error regarding the "default" module missing, you can leave out parent::setUp() in the ControllerTestCase::setUp() method.

    I mentioned it also during my Zend Webinar at http://www.zend.com/en/webinar/Framework/70170000000bUkI-Unit-testing-since-ZF1-8-20110119.flv

    Good luck

    ReplyDelete