Testing Zend Framework Action Controllers With Mocks
In this post I’ll demonstrate a unit test technique for testing Zend Framework Action Controllers using Mock Objects. Unit testing controllers independently has a number of advantages:
- You can develop controllers test-first (TDD).
- It allows you to develop and test all of your controller code before developing any of the view scripts.
- It helps you quickly identify problems in the controller, rather than problems in one of the combination of Model, View and Controller.
The Action Controller I’m going to test has only one method, profileAction():
tests/application/controllers/UserController.php
class UserController extends Zend_Controller_Action
{
public function profileAction()
{
$this->view->userId = $this->_getParam('user_id');
return $this->render();
}
}
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/application.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();
}
}
tests/application/controllers/UserControllerTest.php
require_once TESTS_PATH . '/application/ControllerTestCase.php';
require_once APPLICATION_PATH . '/controllers/UserController.php';
class UserControllerTest extends ControllerTestCase
{
public function testStubRenderMethodCall()
{
$request = $this->getRequest()
->setRequestUri('/user/profile/1')
->setParams(array('user_id'=>1))
->setPathInfo(null);
$response = $this->getResponse();
$this->getFrontController()
->setRequest($request)
->setResponse($response)
->throwExceptions(true)
->returnResponse(false);
$controller = $this->getMock(
'UserController',
array('render'),
array($request, $response, $request->getParams())
);
$controller->expects($this->once())
->method('render')
->will($this->returnValue(true));
$this->assertTrue($controller->profileAction());
$this->assertTrue($controller->view->user_id == 1);
}
}
You can go further making both the tests and the implementation more sophisticated. The main point is that you can build and test a controller in a way that doesn’t require a view script to be written to do so.
Zend Framework Known Issues
By default Zend_Test_PHPUnit_ControllerTestCase sets the redirector exit value to false, leading to unexpected behavior when unit testing your code. For that reason, make sure you always add a return statement after calling a utility method:
class UserController extends Zend_Controller_Action
{
public function profileAction()
{
if (null == $this->_getParam('user_id', null) {
return $this->_redirect('/');
}
return $this->render();
}
}
If you want the Front Controller to throw exceptions, you have no other choice than to overwrite the dispatch method and pass a boolean TRUE to the throwExceptions() method:
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();
}
...
}
The Dispatcher not only violates the DRY principle but also suffers from amnesia. The problem is that it doesn’t store the instance of the Action Controller, instead, it destroys it (Zend_Controller_Dispatcher_Standard Line 305). You can easily get around this issue by extending the standard dispatcher and overwriting the dispatch() method:
class Zf_Controller_Dispatcher_Standard extends Zend_Controller_Dispatcher_Standard
{
...
public function dispatch($url = null)
{
...
Zend_Registry::set('Zend_Controller_Action', $controller);
// Destroy the page controller instance and reflection objects
$controller = null;
}
This will allow you to access the view object after dispatching the request:
class ExampleControllerTest extends ControllerTestCase
{
public function testDefaultActionRendersViewObject()
{
$this->dispatch('/');
$controller = Zend_Registry::get('Zend_Controller_Action');
$this->assertEquals('ExampleController', get_class($controller));
$this->assertTrue(isset($controller->view));
}
Links
PHPUnit: Testing Zend Framework Controllers
PHPUnit: Mock Objects
It is indeed useful to mock the render() method to avoid having to write a view script only to query it, even if I strive for thin controllers that delegates to domain layer.
What I miss here and more generally in Zend Framework is a dependency injection container, since I would naturally define the dependencies of the controller in its constructor. This way I would only need to instantiate the controller class to test it and call its methods.
Giorgio Sironi
November 1, 2009 at 4:58 pm
[...] post: Testing Zend Framework Action Controllers With Mocks Possibly related posts: (automatically generated)Testing Zend Framework Action Controllers With [...]
PHPUnit: Testing Zend Framework Controllers
November 2, 2009 at 1:06 pm
Very nice example of mocking action controllers.
Great job !
Michelangelo van Dam
November 5, 2009 at 8:34 am
Thanks :)
Federico
November 5, 2009 at 9:28 am
[...] Stairs firefox erweiterung für firebug um das aktuelle element das den focus hat hervorzuheben Testing Zend Framework Action Controllers With Mocks « Federico Cargnelutti Zend Framework Controller mit Mocks testen. Interactive CouchDB | Mu Dynamics This is a CouchDB [...]
Max’ Lesestoff zum Wochenende
November 7, 2009 at 6:00 am
Hi. First of all, great article. However, I’m having trouble setting a new Dispatcher. When I try to set one I am getting the message, “No default module defined for this application”. Could you provide an example of how you setup your new Dispatcher?
Karl Purkhardt
November 17, 2009 at 3:40 pm
@Karl,
I have experienced this too a couple of times when using ZF 1.8 series. I managed to bypass this exception by setting the “default” controllers and views directory in APPLICATION_PATH “/application”, although I had a default module specified within the APPLICATION_PATH “/modules” directory.
Hope this works for you as well, but I strongly suggest to upgrade to 1.9.x if it’s possible.
Michelangelo van Dam
November 17, 2009 at 6:08 pm
Yes, I had the same issue and it was solved by doing exactly what Michelangelo said :)
Federico
November 17, 2009 at 10:28 pm
Hi. First of all, great article. However, I’m having trouble setting a new Dispatcher. When I try to set one I am getting the message, “No default module defined for this application”.
ezee
February 10, 2010 at 4:22 pm
You have to set the default controller.
Federico
February 11, 2010 at 10:45 am