Controller testing in CakePHP

Posted by Felix Geisendörfer, on Aug 24, 2006 - in PHP & CakePHP » Testing, Debugging & Refactoring

Deprecated post

The authors of this post have marked it as deprecated. This means the information displayed is most likely outdated, inaccurate, boring or a combination of all three.

Policy: We never delete deprecated posts, but they are not listed in our categories or show up in the search anymore.

Comments: You can continue to leave comments on this post, but please consult Google or our search first if you want to get an answer ; ).

Besides making crazy plans about becoming a pro blogger, redesigning ThinkingPHP and coding on some personal projects for the last couple of days, I've actually found out some more interesting stuff regarding testing in CakePHP as well.

My first idea was to do unit testing on Controllers. I made a little comment about it here and uploaded some of my experimental code for that. In the beginning it looked very promising. I was able to mock Models & Components and add them to the Controller. But the deeper I got into it, the more I realized:

Unit Testing with Controllers is insane

When I started I liked the idea of mocking all the Models & Components, and have them act in certain ways to see how the controller reacts. I still think it would be nice to have, ... but I found that it's not practical at all. When you have a controller with just one Model and one Component, you already have a lot of overhead managing this two mocks, now imagine several bigger Controllers with several Models & Components. You would end up writing 4-5x as much code for unit tests as you would actually write for you application. That's the point for me where "theory about testing" has to go and agility has to kick in.

Looking over at railsland (*sigh*), I saw that them taking a different approach. Instead of testing the Controller as a unit, they would actually dispatch an url and then perform the tests on the most important things manipulated by the controller: assigns, cookies, sessions & flash. They also have a nice set of XML parsing toys to analyze the view output. That way you can perform your testing in a much overhead-reduced & natural manner. But of course it comes at the prices of testing a coupled system instead of a single unit. But then again, it's very agile and should provide reasonable tests to monitor the health of your application so you can easily refactor.

Bringing it to the world of CakePHP

For the last couple of days I've been puzzled by the question of how this could work within CakePHP. The problem was to dispatch an url and get access to the instance of the Controller afterwards. About an hour ago I had an idea: What would happen if I would write an extended Dispatcher and steal the instance from there? It took me a couple minutes until I realized that this is probably a very clean & easy approach and started to write some code. So far it worked out great. Using my new CakePHP bootstraping method that I presented a couple days ago, I was able to write test cases like this:

php
  1. class PostsControllerTest extends ControllerTest
  2. {        
  3.     function test_index()
  4.     {
  5.         $response = $this->get('/posts/index');
  6.         $vars     = $this->controllerVars;
  7.        
  8.         $this->assertTrue(is_array($vars['posts']), 'Posts array is set');
  9.     }
  10. }

If you are courious, that's how my TestDispatcher looked like, but keep in mind that there is some more code in the ControllerTest class which calls the TestDispatcher (and is set as a reference in $TestCase) to make everything run smoothly:

php
  1. class TestDispatcher extends Dispatcher
  2. {
  3.     var $TestCase = null;
  4.    
  5.     function dispatch($url, $additionalParams=array())
  6.     {
  7.         $additionalParams['return'] = 1;
  8.         return parent::dispatch($url, $additionalParams);      
  9.     }
  10.    
  11.     function _invoke (&$controller, $params, $missingAction = false)
  12.     {
  13.         $return = parent::_invoke($controller, $params, $missingAction);
  14.         $this->TestCase->Controller =& $controller;
  15.         return $return;
  16.     }
  17.    
  18.     function cakeError($method, $messages)
  19.     {
  20.         $this->TestCase->cakeError = $method;
  21.     }
  22. }

Where to go?

The next logical step for me is to refactor the testing code I use right now and add a couple of other functions to it. Once I'm done with this, I'll upload it as a PoC on this blog and everybody who's intersted is free to try it out. Depending on the feedback from you guys and the response of the people working on the new offical test suite, I'll try to bring it into a real test suite that we can use for testing our CakePHP apps in future.

--Felix Geisendörfer aka the_undefined

PS: I think it's still possible to mock single Models & Components by overwriting their instances in the Controller in the TestDispatcher::_invoke() function.