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
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
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
Great post! I just got started with PHPUnit myself.
Ben
December 29, 2008 at 3:20 am
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
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
[...] 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
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
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
Great, thanks Oleg :)
Federico
February 6, 2009 at 3:37 pm
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
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
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
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
[...] PHPUnit: Testing Zend Framework Controllers (перевод) [...]
Zend Framework hacks
June 2, 2009 at 9:14 am
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
@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
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
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
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
Thanks for this post!
Ben
June 25, 2009 at 7:46 am
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
I’ll update this post later today, hopefully it will answer your question.
Federico
October 29, 2009 at 11:33 am
thanks Federico! =)
Lois from newLine
October 29, 2009 at 12:00 pm
Done. Post updated on: Oct 29, 2009
Federico
October 29, 2009 at 6:18 pm
[...] PHPUnit: Testing Zend Framework Controllers PHPUnit: Mock Objects Possibly related posts: (automatically generated)Unit-testing ZF Controllers without Zend_TestU.S. Condemns Nuclear Test [...]
Testing Zend Framework Action Controllers With Mocks « Federico Cargnelutti
November 1, 2009 at 1:40 pm
[...] ( 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 [...]
How to properly use Zend_Test and Zend_AMF - Zend Framework Forum
January 29, 2010 at 1:58 pm
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
[...] PHPUnit: Testing Zend Framework Controllers PHPUnit: Mock Objects [...]
PHP-help » Testing Zend Framework Action Controllers With Mocks
March 11, 2010 at 6:11 pm
[...] 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