I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 638 posts at DZone. You can read more from them at their website. View Full User Profile

Zend_Test for Acceptance TDD

05.19.2010
| 4059 views |
  • submit to reddit

Acceptance Test-Driven Development is an Agile technique that extends the test-first approach to the development of the front end of an application. The mechanics of Acceptance TDD are clear: first you write a test which defines the goal of your development, which is basically the feature you're adding to your application. As with all TDD variants, this test must fail.

After adding a failing test, you add only the necessary code to make it pass, and once the bar is green you can refactor seamlessly. Note that in this case, getting the bar to green may require many smaller TDD cycles where you write unit tests and make them pass; these tests are much more fine-grained, and Acceptance TDD helps you decide which unit tests to write (hint: the ones that will serve your front end to make the original acceptance test green).

Acceptance TDD has all the advantages of TDD applied at a large-scale level. Probably the greatest advantage is in providing the definition of what is considered done: a feature is finished when all its acceptance tests pass. Another pro of employing Acceptance TDD is to easily craft a regression test suite at the end-to-end level, which discovers most of the wiring bugs (object that are not instantiated or are created incorrectly, different contracts on the provider and client sides).

The code that constitute acceptance tests can be verbose and must be particularly well maintained and refactored to avoid fragility of the test suite. A declarative layer must be built upon the basic test framework, to eliminate all the duplication from the test code and be able to change the specifics of the front end (in web applications CSS classes and ids, or the overall DOM structure) in only two places: one test class and the production code.

Zend_Test

Zend_Test is a component of Zend Framework which simulates the request/response cycle for applications built with this framework MVC stack (primarily Zend_Controller and Zend_View). All tests are executed in a single memory process (no round trip to HTTP servers or similar), but are really end-to-end tests as they exercise the application from its URI entry point. Currently the only implementation of Zend_Test test cases is for PHPUnit, and it takes the form of a custom test case with additional methods for sending fake HTTP requests and perform response-related assertions.

Some components are stubbed out automatically due to the nature of the native PHP Api (one for all: Zend_Session, which becomes an array instead of actually setting cookies and starting sessions), while others must be stubbed out at will to catch computation that goes too far away from the application itself (database adapters, substituted with lightweight versions which can be thrown away at the end of the test run, or Zend_Mail, substituted with a stub mailer that catches the outcoming mails so that assertions can be executed upon them.)

The capabilities of Zend_Test are many, as it takes advantage of the MVC stack architecture and of its wrapping of basic PHP functions. For example, redirects or error headers are catched instead of being sent with header() (which may cause errors in a command line environment).

The fake requests, obviously can set custom GET or POST parameters or any kind of HTTP headers. The URLs are always considered relative to the document root of the application, so that the test suite is portable and can be executed without changes or configuration on different machines (without any Apache instance running).

This is a basic test which checks that a page is loaded (by default via GET) and it contains some text in particular tags specified via CSS selectors:

class Example_CrudTest extends Example_AbstractTest
{
public function testFactoryIsLoaded()
{
$this->dispatch('/naked-php/view/type/service/object/Example_Model_PlaceFactory');
$this->assertQueryContentContains('#methods a', 'createCity');
$this->assertQueryContentContains('#methods a', 'createPlaceCategory');
$this->assertQueryContentContains('#methods a', 'createPlace');
$this->assertNotQuery('#object .button.edit');
$this->assertNotQuery('#object .button.remove');
}
}

Even Ajax requests can be simulated with the X-Requested-With custom header, recognized by the MVC stack. For example, this test checks that when a page is included via Ajax calls, it still calls the right action but the result does not contain the layout:

class Otk_Content_SectionControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
    public function testIntegration()
    {
        $this->request->setMethod('GET')
                      ->setHeader('X-Requested-With', 'XMLHttpRequest');
        $this->dispatch("/content/article/{$articleSlug}/add?format=html");
        $this->assertModule('content');
        $this->assertController('article');
        $this->assertQuery('form');
        $this->assertNotQuery('div#container');
    }
}

We can also simulate POST requests of course, and check that after a successful execution they redirect to a result page:

    public function testCityFactoryMethodCreatesCityInstance()
{
$this->getRequest()
->setMethod('POST')
->setPost(array(
'name' => 'New York'
));
$this->dispatch('/naked-php/call/type/service/object/Example_Model_PlaceFactory/method/createCity');
$this->assertRedirectTo('/naked-php/view/type/entity/object/1');
}

There are some gotchas in using Zend_Test however. First, test methods execution are isolated as all PHPUnit tests are. The session is reset along with the other components in setUp(), so for example authentication won't be maintained in different test methods. To perform multiple requests in the same test method, for example adding an entity and check that it is listed in another page, remember to reset the request and the response before dispatching another action:

$this->resetRequest()
->resetResponse();

Of course you can also truncate the database or regenerate the fixtures in the setUp() method, but make sure that you call parent::setUp() to leverage the original test case work.

Another possible issue is that the front controller is configured in such a way that it won't throw exceptions: exceptions will be shown in an error page instead of bubbling up directly in PHPUnit. To avoid this behavior, you can add:

$this->frontController->throwExceptions(true);

to your setUp() method. The typical setUp() looks like this:

abstract class Example_AbstractTest extends Zend_Test_PHPUnit_ControllerTestCase
{
public function setUp()
{
$application = new Zend_Application(
'testing',
APPLICATION_PATH . '/configs/application.ini'
);
$this->bootstrap = array($application, 'bootstrap');
parent::setUp();
$this->frontController->setParam('bootstrap', $application->getBootstrap());
$this->frontController->throwExceptions(true);
}
}

where a Zend_Application is instantiated with the testing environment configuration. The bootstrap is not passed to the front controller by this code, so we have to manually set it here if your controllers access it. In production code you won't experience this problem.

If you are curious about Zend_Test or didn't get how a code snippet works, feel free to ask questions in the comments.

Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)