PHPUnit: Testing Zend Framework Controllers
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:
app/
config/
controllers/
ExampleController.php
models/
views/
lib/
Zend/
public/
tests/
controllers/
AllTests.php
ExampleControllerTest.php
lib/
AllTests.php
bootstrap.php
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 [ Open in Codepad ]
<?php
/* Start output buffering */
ob_start();
/* Report all errors directly to the screen for simple diagnostics in the dev environment */
error_reporting( E_ALL | E_STRICT );
ini_set('display_startup_errors', 1);
ini_set('display_errors', 1);
date_default_timezone_set('Europe/London');
/* Determine the root and library directories of the application */
$appRoot = dirname(__FILE__) . '/..';
$libDir = "$appRoot/lib";
$path = array($libDir, get_include_path());
set_include_path(implode(PATH_SEPARATOR, $path));
define('APPLICATION_PATH', $appRoot . '/app');
define('APPLICATION_ENVIRONMENT', 'dev');
require_once "Zend/Loader.php";
Zend_Loader::registerAutoload();
$front = Zend_Controller_Front::getInstance();
$front->throwExceptions(true);
$front->setParam('noViewRenderer', true);
$front->setParam('env', APPLICATION_ENVIRONMENT);
$front->setRequest(new Zend_Controller_Request_Http());
$front->returnResponse(true);
$router = $front->getRouter();
include APPLICATION_PATH . '/config/routes.php';
$router->addRoutes($routes);
$router->setParams($front->getParams());
$dispatcher = $front->getDispatcher();
$dispatcher->setParams($front->getParams());
$dispatcher->setResponse($front->getResponse());
$router->route($front->getRequest());
Please note that disabling the ViewRenderer helper is optional. However, you should know that using the Zend_Controller_Action_Helper_ViewRenderer class can result in performance degradation. More info here.
The PHPUnit_Framework_TestSuite class of the PHPUnit framework allows you to organize tests into a hierarchy of test suites:
tests/AllTests.php [ Open in Codepad ]
<?php
require_once dirname(__FILE__) . '/bootstrap.php';
require_once dirname(__FILE__) . '/controllers/AllTests.php';
class AllTests
{
public static function main()
{
$parameters = array();
PHPUnit_TextUI_TestRunner::run(self::suite(), $parameters);
}
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('My Application');
$suite->addTest(ControllersAllTests::suite());
return $suite;
}
}
AllTests::main();
tests/controllers/AllTests.php [ Open in Codepad ]
<?php
require_once dirname(__FILE__) . '/ExampleControllerTest.php';
class ControllersAllTests
{
public static function main()
{
PHPUnit_TextUI_TestRunner::run(self::suite());
}
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('My Application - Controllers');
$suite->addTestSuite('ExampleControllerTestCase');
return $suite;
}
}
Writing unit tests
For some strange reason this part is not documented in the manual. Here’s what you need to do before writing a test:
- Include the controller you want to test.
- Extend the Action controller.
- Reset the instance of the Front controller.
- Set the path to the Example action controller.
- Set the Request and Response objects.
- Create an instance of the Example action controller.
Example:
tests/controllers/ExampleControllerTest.php [ Open in Codepad ]
<?php
require_once APPLICATION_PATH . '/controllers/ExampleController.php';
class ExampleControllerTest extends ExampleController
{
public function __construct($url = null)
{
$front = Zend_Controller_Front::getInstance();
$front->resetInstance();
$front->setControllerDirectory(APPLICATION_PATH . '/controllers');
$front->setRequest(new Zend_Controller_Request_Http($url));
$front->setResponse(new Zend_Controller_Response_Http());
parent::__construct($front->getRequest(), $front->getResponse());
}
}
All the magic happens inside the ExampleControllerTest class. It makes the Action controller believe it’s being dispatched by the Front controller. This is the only way to create an instance of the action controller without dispatching the request. Getting an instance of the action controller gives you more control and flexibility, specially when testing web services.
That’s it, 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.
[ Open in Codepad ]
require_once APPLICATION_PATH . '/controllers/ExampleController.php';
class ExampleControllerTest extends ExampleController
{
...
}
class ExampleControllerTestCase extends PHPUnit_Framework_TestCase
{
public function testDefaultAction()
{
$controller = new ExampleControllerTest();
$isDispatched = $controller->indexAction();
$this->assertTrue($isDispatched);
}
public function testFirstAction()
{
$url = 'http://localhost/example/first';
$controller = new ExampleControllerTest($url);
$controller->firstAction();
$errorMsg = $controller->getRequest()->getParam('error_message', null);
$this->assertEquals(null, $errorMsg);
}
public function testGetParameterName()
{
$url = 'http://localhost/example/first/fed';
$controller = new ExampleControllerTest($url);
$name = $controller->getRequest()->getParam('name', null);
$this->assertEquals('fed', $name);
}
public function testGetNameMethod()
{
$url = 'http://localhost/example/first/fed';
$controller = new ExampleControllerTest($url);
$name = $controller->getName();
$this->assertEquals('fed', $name);
}
}
Running tests
federico@tests$ phpunit AllTests PHPUnit 3.3.8 by Sebastian Bergmann. ..... Time: 0 seconds OK (4 tests, 4 assertions)
If there are test failures, you’ll see full details about which tests failed. Optionally, you can plug Phing into Hudson and automate this task. Any questions let me know.
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