Federico Cargnelutti

Simple is better than complex. Complex is better than complicated. | @fedecarg

Testing Zend Framework Controllers

with 31 comments

Post updated on: Oct 29, 2009

Testing a Web application is a complex task, because a Web application is made of several layers of logic. Unit testing a Zend Framework controller can be very difficult, specially for those who are not familiar with the Zend Framework.

You can test your action controllers using Zend_Test and/or PHPUnit. Zend_Test allows you to simulate requests, insert test data, inspect your application’s output and generally verify your code is doing what it should be doing. It’s up to you to decide which one to use. Even though this choice can be confusing, you can use both. If you’re just getting started with testing, using Zend_Test will probably get you started faster.

The PHPUnit framework will probably feel very familiar to developers coming from Java. It’s inspired by Java’s JUnit, so you’ll feel at home with this method if you’ve used JUnit or any test framework inspired by JUnit.

Of course, no one stops you from using both systems side-by-side (even in the same app). In the end, most projects will eventually end up using both. Each shines in different circumstances.

Using PHPUnit

First, you need to create the directory structure:

application/
    config/
    controllers/
        ExampleController.php
    models/
    views/
library/
    Custom/
    Zend/
public/
tests/
    application/
        controllers/
            ExampleControllerTest.php
        ControllerTestCase.php
    library/
    log/
        coverage/
    bootstrap.php
    phpunit.xml

A test suite needs some environmental information, and this information is usually found in the bootstrap.php file. The biggest difference between this file and the one you use in you application, is that the Front Controller doesn’t dispatch the Request Object:

tests/bootstrap.php

error_reporting( E_ALL | E_STRICT );
ini_set('display_startup_errors', 1);
ini_set('display_errors', 1);
date_default_timezone_set('Europe/London');

define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));
define('APPLICATION_ENV', 'loc');
define('LIBRARY_PATH', realpath(dirname(__FILE__) . '/../library'));
define('TESTS_PATH', realpath(dirname(__FILE__)));

$_SERVER['SERVER_NAME'] = 'http://localhost';

$includePaths = array(LIBRARY_PATH, get_include_path());
set_include_path(implode(PATH_SEPARATOR, $includePaths));

require_once "Zend/Loader.php";
Zend_Loader::registerAutoload();

Zend_Session::$_unitTestEnabled = true;
Zend_Session::start();

tests/phpunit.xml

<phpunit bootstrap="./bootstrap.php" colors="false">
    <testsuite name="ApplicationTestSuite">
        <directory>./application/</directory>
        <directory>./library/</directory>
    </testsuite>
    <filter>
        <whitelist>
            <directory suffix=".php">../application</directory>
            <directory suffix=".php">../library/Custom</directory>
            <exclude>
                <directory suffix=".phtml">../application/views</directory>
                <file>../application/Bootstrap.php</file>
            </exclude>
        </whitelist>
    </filter>
    <logging>
        <log type="coverage-html" target="./log/coverage" charset="UTF-8"
             yui="true" highlight="false" lowUpperBound="35" highLowerBound="70"/>
    </logging>
</phpunit>

Note: If you are using a version of PHPUnit >= 3.4, you need to add the ”testsuites” tag.

tests/application/ControllerTestCase.php

class ControllerTestCase extends Zend_Test_PHPUnit_ControllerTestCase
{
    public $application;

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

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

    public function tearDown()
    {
        Zend_Controller_Front::getInstance()->resetInstance();
        $this->resetRequest();
        $this->resetResponse();

        $this->request->setPost(array());
        $this->request->setQuery(array());
    }

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

If you want the Front Controller to throw all the exceptions, you have no other choice than to overwrite the dispatch method and pass a boolean TRUE to the throwExceptions() method. For example:

class ControllerTestCase extends Zend_Test_PHPUnit_ControllerTestCase
{
    ...

    public function dispatch($url = null)
    {
        // redirector should not exit
        $redirector = Zend_Controller_Action_HelperBroker::getStaticHelper('redirector');
        $redirector->setExit(false);

        // json helper should not exit
        $json = Zend_Controller_Action_HelperBroker::getStaticHelper('json');
        $json->suppressExit = true;

        $request = $this->getRequest();
        if (null !== $url) {
            $request->setRequestUri($url);
        }
        $request->setPathInfo(null);

        $this->getFrontController()
             ->setRequest($request)
             ->setResponse($this->getResponse())
             ->throwExceptions(true)
             ->returnResponse(false);

        $this->getFrontController()->dispatch();
    }

    ...
}

Writing unit tests

Time to create our first test suite. A test suite is a class inherited from PHPUnit_Framework_TestCase containing test methods, identified by a leading “test” in the method name.

tests/application/controllers/ExampleControllerTest.php

<?php
require_once TESTS_PATH . '/application/ControllerTestCase.php';
require_once APPLICATION_PATH . '/controllers/ExampleController.php';

class ExampleControllerTest extends ControllerTestCase
{
    public function testDefaultShouldInvokeIndexAction()
    {
        $this->dispatch('/');
        $this->assertController('index');
        $this->assertAction('index');
    }

    public function testViewObjectContainsStringProperty()
    {
        $this->dispatch('/');

        $controller = new ExampleController(
            $this->request,
            $this->response,
            $this->request->getParams()
        );
        $controller->indexAction();

        $this->assertTrue(isset($controller->view->string));
    }
}

To create an instance of the Action Controller, you need to dispatch the request and pass the Request/Response objects and parameters to the Action Controller.

tests/application/controllers/ErrorControllerTest.php

require_once TESTS_PATH . '/app/ControllerTestCase.php';
require_once APPLICATION_PATH . '/controllers/ErrorController.php';

class ErrorControllerTest extends ControllerTestCase
{
    public function testExceptionIsAnInstanceOfZendControllerException()
    {
        $e = new stdClass();
        $e->type = Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER;
        $e->exception = new Zend_Controller_Exception('Invalid controller');
        $e->request = $this->request;

        $this->request->setParam('error_handler', $e);
        $this->request->setParam('controller', 'error');
        $this->request->setParam('action', 'error');

        $controller = new ErrorController(
            $this->request,
            $this->response,
            $this->request->getParams()
        );
        $controller->errorAction();
        $isInstanceOf = $controller->view->exception instanceof Zend_Controller_Exception;

        $this->assertTrue($isInstanceOf);
    }
}

Running tests

$ cd /path/to/tests
$ phpunit
PHPUnit 3.3.8 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests, 4 assertions)

If there are test failures, you’ll see full details about which tests failed. Optionally, you can use Phing and Hudson to automate this task.

Related post:
Testing Zend Framework Action Controllers With Mocks
Setting up continuous integration for PHP using Hudson and Phing

About these ads

Written by Federico

December 27, 2008 at 10:51 am

31 Responses

Subscribe to comments with RSS.

  1. I’m confused. Why are you extending the action? Couldn’t you simply dispatch through the front controller? That’s what I’ve done in my controller tests – as per the docs.

    Jani Hartikainen

    December 27, 2008 at 3:36 pm

  2. Well, I’m not dispatching the request, that’s why. It’s the only way to create an instance of the action controller without dispatching the request. Getting an instance of the action controller gives me more control and flexibility, specially when creating web services and using sendJson(). Keep in mind that sendJson(), for example, does not render a view and terminates the execution of the script after sending the response.

    Federico

    December 28, 2008 at 1:39 am

  3. Great post! I just got started with PHPUnit myself.

    Ben

    December 29, 2008 at 3:20 am

  4. The doc for Zend_Controller_Action says:
    void __construct (Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, [ $invokeArgs = array()])
    how is:
    $url = ‘http://localhost/example/first/fed’;
    $controller = new ExampleControllerTest($url);
    supposed to work?
    How can the url parameters be parsed without dispatching a request (the routes do not exist in this example)?

    Piccolo Principe

    December 29, 2008 at 7:48 pm

  5. Hi,

    From the manual: “In order to run a parent constructor, a call to parent::__construct() within the child constructor is required.”

    ExampleControllerTest::__contruct() runs Zend_Controller_Action::__construct(), passes the URI to Zend_Controller_Request_Http and to Zend_Uri. That’s how the Zend_Uri object populates itself.

    Routes are added in the bootstrapper, however I’m not testing the routing process. For that I use Zend_Test.

    Federico

    December 29, 2008 at 10:19 pm

  6. [...] the PHP::Impact blog there’s a recent post looking at using the popular unit testing PHP framework PHPUnit to test Zend Framework controllers. [...]

    How2Pc

    December 30, 2008 at 3:06 am

  7. Frederico,
    this came at just the right time! I’ve been setting up netbeans with unit testing, and I was going to add Zend_Test into the mix this week.

    Thanks!

    j

    Jon Lebensold

    January 2, 2009 at 9:21 pm

  8. Hi Federico,

    Very nice article!

    I translated it to Russian, if you don’t mind. You can see the translation on my blog – http://lobach.info/develop/zf/phpunit-testing-zend-framework-controllers/

    Oleg Lobach

    February 6, 2009 at 11:15 am

  9. Great, thanks Oleg :)

    Federico

    February 6, 2009 at 3:37 pm

  10. Hi Federico

    In bootstrap.php, you mentioned the following command:

    include APPLICATION_PATH . ‘/config/routes.php’;
    $router->addRoutes($routes);

    Does the variable $routes come from routes.php? Could you give some tips how to build the routes.php file?

    Good article, thank you for your work.

    Henry Lu

    February 26, 2009 at 5:02 pm

  11. That’s right, the variable $routes is defined inside the routes.php file. Not the best way of doing it, I know, but it’s just an example.

    Federico

    February 27, 2009 at 12:58 am

  12. Hallo!
    My name is Maria and I am an php-developer who from now want to use Zend Framework and use PHPUnit.
    A very nice article!! But I stuck my head into problems. I can run the phpunit test via command line:

    C:\projects\Prova\php\phpunit2>php stringtest.php

    TestCase ArrTest->testNewArrayIsEmpty() passed
    TestCase ArrTest->testArrayContainsAnElement() passed

    BUT
    when I follow your exemple, I got the errormessage:
    Could not open input file: .\pear\PHPUnit2\TextUI\TestRunner.php

    Seems to be som config-problem but I cannot find it.
    I’ll be so happy if you could help me.
    Best regards,
    Maria

    Maria Schön

    April 23, 2009 at 7:26 am

  13. I want to use Zend+PHPUnit, but it’s so hard.
    My code is equal this post.
    But throw an error: action helper by name search not found

    Obs.: I don’t use ZendRoutes but .htaccess.
    Anybody have a light?

    Rafael

    June 1, 2009 at 9:16 pm

  14. [...] PHPUnit: Testing Zend Framework Controllers (перевод) [...]

    Zend Framework hacks

    June 2, 2009 at 9:14 am

  15. Hi Federico,

    Thanks for the article! It was very helpful in guiding me through the setup of Zend+PHPUnit.

    I had to change the method for calling ExampleControllerTest; I couldn’t extend the constructor as indicated because it has to use the same interface as the original. Made ExampleControllerTestCase::getExampleControllerTest($url) method instead.

    I also got rid of the routes, seems to work using the auto-routing.

    Lauxa

    June 4, 2009 at 12:34 pm

  16. @Rafael the idea of testing your code is to find errors, and “Action helper by name not found” *is* one of them :) Make sure you always check if a helper class is set before using it.

    For example, if you do this::
    $this->_helper->layout->disableLayout();

    Without checking:
    if ($this->_helper->hasHelper(‘layout’)) {
    }

    Then you get the following error message:
    “Action Helper by name Layout not found”

    Federico

    June 4, 2009 at 4:32 pm

  17. Thanks Lauxa. You may need the routes if you decide to use an action helper or plugin that redirects or creates an URL, but I’m not 100% sure.

    Cheers

    Federico

    June 4, 2009 at 4:34 pm

  18. Hi Federico,

    Another comment, if you are using the view in your controllers, there is an error when you set up the bootstrap with this command –

    $front->setParam('noViewRenderer', true);

    – because now $view is undefined. My solution was to write an empty class called FakeView.php, drop it into test/controllers directory, and change the getExampleControllerTest method as such:

    public function getExampleControllerTest($url = null) {
    $request = new Zend_Controller_Request_Http($url);
    $response = new Zend_Controller_Response_Http();
    $controller = new ScheduleController($request, $response);
    $controller->view = new FakeView();
    return $controller;
    }

    I don’t know if it’s the best solution, but now the $view variable doesn’t break in the controllers and I still get output to the screen.

    Lauxa

    June 11, 2009 at 5:20 pm

  19. If you disable the view rendered helper, then you have to initiate the view object first before assigning values. For example:


    $this->initView();
    $this->view->foo = 'value';
    $this->render();

    Federico

    June 11, 2009 at 7:41 pm

  20. Thanks for this post!

    Ben

    June 25, 2009 at 7:46 am

  21. Hi, my problem is how to launch the test. In your last step, when you create the test suite. Where have to been this file?

    Thanks a lot

    Lois from newLine

    October 29, 2009 at 11:22 am

  22. I’ll update this post later today, hopefully it will answer your question.

    Federico

    October 29, 2009 at 11:33 am

  23. thanks Federico! =)

    Lois from newLine

    October 29, 2009 at 12:00 pm

  24. Done. Post updated on: Oct 29, 2009

    Federico

    October 29, 2009 at 6:18 pm

  25. [...] PHPUnit: Testing Zend Framework Controllers PHPUnit: Mock Objects Possibly related posts: (automatically generated)Unit-testing ZF Controllers without Zend_TestU.S. Condemns Nuclear Test [...]

  26. [...] ( false ); //Return what the amf server delivers echo($server->handle()); Many of the examples of using Zend_Test I have seen use Zend_Test_PHPUnit_ControllerTestCase If I have no controllers is [...]

  27. Very useful! Thank you. Especially creating an instance of an ErrorController (i.e. $controller = new ErrorController (…)) in a test unit. I did not know you can do that. Very, very nice:-)!

    W

    February 9, 2010 at 12:10 pm

  28. [...] PHPUnit: Testing Zend Framework Controllers PHPUnit: Mock Objects [...]

  29. [...] Basis für mein Setup möchte ich auf den Blogeintrag von Federico Cargnelutti geholfen, auf das ich an dieser Stelle verweisen möchte. In dem Eintrag werden alle nötigen [...]

    wortzwei

    April 9, 2010 at 2:48 pm

  30. [...] solution here for others. First of all, to my problem, these two articles for most enlightening: #1 and #2. First modification. When instantiating, I got errors saying that some class wasn't [...]

    zfforums

    January 27, 2011 at 10:39 am

  31. 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.

    Is it necessary to consider the code coverage?

    codef0rmer

    March 29, 2011 at 12:36 pm


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 )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 1,033 other followers