View Source

<h2>The Problem</h2>
<p>Since I originally undertook the MVC refactoring project in the fall of 2006, I've been touting the fact that you can test ZF MVC projects by utilizing the Request and Response objects; indeed, this is what I actually did to test the Front Controller and Dispatcher. However, there is not currently an easy way to do so; the default request and response objects make it difficult to easily and quickly setup tests, and the methods introduced into the front controller to make it testable are largely undocumented. The only resource developers currently have for this sort of testing is the unit tests themselves.</p>

<p>Testing against the response object is somewhat tedious; you have to be regex guru in many cases, and the testing itself includes many calls to the response object in order to grab the values to test against.</p>

<p>Testing against databases becomes even more tedious. You need to either have a test database setup against which you can point your test scaffold, or you have to provide mock objects. Both can be time consuming.</p>

<h2>What should be testable</h2>

<p>My contention is that the following application areas should be testable:</p>

<ul>
<li>Response status: HTTP response codes (assume 200, but several methods will set the response code in the response object)</li>
<li>Response headers: presence of headers and content of headers</li>
<li>Content:
<ul>
<li>what nodes are present, selected by DOM id and/or CSS class selectors</li>
<li>content of selected nodes, both straight string comparisons and by regex</li>
</ul>
</li>
</ul>


<p>In most cases, you don't need to go into terribly fine-grained detail, but you need to verify that particular elements or types of elements are present in the response, as well as potentially match small segments of the content. The response status is generally desirable, and often you need to test where an action may redirect.</p>

<p>When selecting nodes in XML or XHTML, the &quot;gold standard&quot; is represented by Prototype's $$() function, and Dojo Toolkit's dojo.query() method. Each of these utilize CSS selectors to qualify the nodes to select; CSS selectors allow us to specify specific node types, node ids, as well as the values contained in the 'class' attribute of nodes; additionally, it has a concept of inheritance, to indicate parent/child relations. This kind of specificity can allow a developer to check that <em>types</em> of content are present, instead of the <em>specific</em> content &ndash; which may be variable, due to having unreliable sources (database, for instance).</p>

<p>With databases, you will need to test whether or not new rows were created, whether or not existing rows were updated, and what the values of rows and/or individual row fields are. PHPUnit, as of version 3.2.0, has a special test case available, PHPUnit_Extensions_Database_TestCase, that ports Java's DBUnit to PHP, and automates most of these operations. We would add some convenience assertions to verify lower level information such as whether or not a row was added or deleted.</p>

<h2>The Proposal</h2>
<p>I'm proposing several items. First, a component for selecting against DOM artifacts:</p>

<ul>
<li>Zend_Dom_Query</li>
</ul>


<p>This class is named to indicate first, its function, and second, an affinity for dojo.query() (since $$ is a name that's hard to translate to PHP). This component would be utilized to parse response body content.</p>

<p>Second, a test case for controllers that extends PHPUnit_Extensions_Database_TestCase:</p>

<ul>
<li>Zend_PHPUnit_ControllerTestCase (extends PHPUnit_Extensions_Database_TestCase)</li>
</ul>


<p>This test case would provide scaffolding for bootstrapping the MVC application being tested, methods for dispatching a request and retrieving the response, and a number of assertions for running against the request.</p>

<p>The assertions include:</p>
<ul>
<li>assertSelect()</li>
<li>assertResponse()</li>
<li>assertRedirect()</li>
<li>assert(Not)AttributeExists($domNode, $attr)</li>
<li>assert(Not)AttributeEquals($domNode, $attr, $value)</li>
<li>assertDataSetInsert($table)</li>
<li>assertDataSetDelete($table, $id)</li>
<li>assertDataSetUpdated($table, $id)</li>
</ul>


<p>Finally, custom request/response objects would also be created to facilitate testing (in particular, adding accessors to set request data), with corresponding interfaces for the functionality required by testing. This latter would faciliate custom request/response objects provided by the developer.</p>

<ul>
<li>Zend_PHPUnit_ControllerTestCase_Request_Interface</li>
<li>Zend_PHPUnit_ControllerTestCase_Request extends Zend_Controller_Request_Abstract implements Zend_PHPUnit_ControllerTestCase_Request_Interface</li>
<li>Zend_PHPUnit_ControllerTestCase_Response_Interface</li>
<li>Zend_PHPUnit_ControllerTestCase_Response extends Zend_Controller_Response_Abstract implements Zend_PHPUnit_ControllerTestCase_Response_Interface</li>
</ul>


<p>Now, for an example:</p>
<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
class FooControllerTest extends Zend_PHPUnit_ControllerTestCase
{
public $bootstrap = dirname(__FILE__) . '/../bootstrap.php';

public function testArchivesRouteShouldContainNabble()
{
$this->dispatch('/archives');
$this->assertResponse(200);
$this->assertSelect('#nabble');
}

public function testNabbleInArchivesShouldContainScript()
{
$this->testArchivesRouteShouldContainNabble();
$this->assertSelect('#nabble', '<script');
}

public function testAddPersonShouldUpdateDatabase()
{
$this->getRequest()->setPost($this->newPersonData);
$this->dispatch('/person/add');
$this->assertResponse(200);

$this->assertDataSetInsert('person');
}
}
]]></ac:plain-text-body></ac:macro>

<h2>Design Notes</h2>

<h3>bootstrap() Method</h3>

<p>By default, checks if $bootstrapFile is set, and if so, includes that file.</p>

<p>Override the method to provide specific functionality.</p>

<p>If using a bootstrap file, the file should not dispatch. Additionally, users should be aware that custom request and response objects will be set in the front controller if none are set in the TestCase.</p>

<h3>reset() Method</h3>

<p>Reset's object state of front controller, and clears request/response objects.</p>

<h3>setUp() Method</h3>

<p>By default, calls reset() and bootstrap(). When overriding, should call parent::setUp().</p>

<h3>dispatch($uri) Method</h3>

<p>Sets $uri in request object, and then dispatches front controller.</p>

<p>Sets returnResponse() in front controller to ensure response is returned.</p>

<h3>Sessions</h3>
<p>May need to create a Zend_Session replacement that can be used within tests. Must be compatible with Zend_Session_Namespace. Will be an object store to allow you to check the contents of the session to see if data has been set.</p>

<h3>New Assertions</h3>

<h4>assertSelect()</h4>

<p>Basically, this operates similar to Prototype's $$() function; you pass it CSS selector notation, which allows for ids, classes, and inheritance. Based on the number of arguments you pass and context, you assert different things:</p>

<ul>
<li>assertSelect('#foo') - foo id exists</li>
<li>assertSelect('#foo', false) - foo id does not exist</li>
<li>assertSelect('#foo', 'bar') - content of foo contains 'bar'</li>
<li>assertSelect('#foo', 'bar', false) - content of foo does not contain 'bar'</li>
<li>assertSelect('#foo', '/bar/i') - regex match on content of foo</li>
<li>assertSelect('#foo', '/bar/i', false) - negative regex match on content of foo</li>
<li>assertSelect('td#foo&gt;tr', 3) - at least three rows in the foo table</li>
<li>assertSelect('td#foo&gt;tr', 3, 'max') - no more than three rows in the foo table</li>
</ul>


<h4>assertResponse(), assertRedirect()</h4>

<p>Tests various properties of the response:</p>

<ul>
<li>assertResponse(200) - 200 OK response code</li>
<li>assertResponse(404) - assert page not found</li>
<li>assertResponse(500) - assert application error</li>
<li>assertResponse('X-App-Id') - X-App-Id header in response</li>
<li>assertResponse('X-App-Id', false) - X-App-Id header not in response</li>
<li>assertResponse('X-App-Id', 'bar') - X-App-Id header contains 'bar'</li>
<li>assertResponse('X-App-Id', '/bar/i') - regex match on X-App-Id header</li>
<li>assertRedirect() - response is a redirect</li>
<li>assertRedirect(false) - response is not a redirect</li>
<li>assertRedirect('/foo') - response redirects to /foo</li>
<li>assertRedirect('/foo', false) - response does not redirect to /foo</li>
<li>assertRedirect('|^/foo|') - regex match on response redirect</li>
<li>assertRedirect('|^/foo|', false) - negative regex match on response redirect</li>
</ul>