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

Zend Framework Packageizer

Jani Hartikainen created one of the most innovative Zend Framework applications of 2008, the Packageizer. It allows you to choose a ZF component with all its dependencies and download it as a PHAR or ZIP file.

The Zend Framework community is getting bigger and more creative, and users are building more and better web applications. But, unfortunately, no one knows where these applications are or who developed them.

Community to Zend, community to Zend, do you copy? Over.

Netbooks: Microsoft’s biggest worry

Netbooks will account for about a third of all PC growth this year, according to Citigroup. They are a real threat to Microsoft. Clearly, the future is in netbooks and that has Microsoft worried. Microsoft isn’t just worried about ceding 30% of the netbook market to Linux, it’s also worried that if people get used to running Linux on netbooks, they’ll consider buying Linux on desktop PCs as well.

According to Dickie Chang, this gives users a chance to see and try something new, showing them there is an alternative.

Happy New Year!

Memcached consistent hashing mechanism

If you are using the Memcache functions through a PECL extension, you can set global runtime configuration options by specifying the values within your php.ini file. One of them is memcache.hash_strategy. This option sets the hashing mechanism used to select and specifies which hash strategy to use: Standard (default) or Consistent.

It’s recommended that you set to Consistent to allow servers to be added or removed from the pool without causing the keys to be remapped to other servers. When set to standard, an older strategy is used that potentially uses different servers for storage.

With PHP, the connections to the memcached instances are kept open as long as the PHP and associated Apache instance remain running. When adding a removing servers from the list in a running instance, the connections will be shared, but the script will only select among the instances explicitly configured within the script.

So, to ensure that changes to the server list within a script do not cause problems, make sure to use the consistent hashing mechanism.

A graph is worth a thousand words

phpCallGraph is a tool to generate static call graphs for PHP source code. Such a graph visualizes the call dependencies among methods or functions of an application. Arrows represent calls from one method to another method. Classes are drawn as rectangles containing the respective methods. The graphs can be leveraged to gain a better understanding of large PHP applications or even to debunk design flaws in them.

It is also possible to visualize calls to internal functions of PHP and to some extend call dependencies among different classes. Have a look at the example call graphs to get an impression.

Great tool, thanks Falko!

Add live support to your Website

Have you ever thought, “If only I could allow my visitors to chat live with me while they browse my site?” Now you can.  Open Web Messenger is an open source live support chat application. It enables customers and visitors to chat with an operator and get support.

Main features

  • Unlimited operators, chats, and users
  • Priority queue of visitors
  • Chat button for email signatures
  • Differing buttons on same website
  • Reconnect automatically if the internet connection went down

Server requirements

  • PHP (5.x and above) with MySQL support
  • MySQL 5.0 and above

Links

Top 100 Blogs for Developers

PHPImpact makes it to the top 100!

What defines the popularity of a blog? The ranking by Google? Traffic statistics? Feed subscriptions? Feedback from readers? Links from other sites and blogs? The answer is of course… All of them!

Jurgen Appelo announced the new edition of the Top 100 Blogs for Developers. The list is based on a weighed average of each blog’s Google PageRank, Alexa Rank, Technorati Authority, RSSMicro FeedRank, Google hits, and blog comments.

Top 100 Blogs for Developers