Testing Zend Framework Controllers

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

31 thoughts on “Testing Zend Framework Controllers

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

  2. 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)?

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

  4. Pingback: How2Pc

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

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

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

  8. 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?

  9. Pingback: Zend Framework hacks

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

  11. @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”

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

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

  14. 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();

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

  16. Pingback: Testing Zend Framework Action Controllers With Mocks « Federico Cargnelutti

  17. Pingback: How to properly use Zend_Test and Zend_AMF - Zend Framework Forum

  18. 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:-)!

  19. Pingback: PHP-help » Testing Zend Framework Action Controllers With Mocks

  20. Pingback: wortzwei

  21. Pingback: zfforums

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