Testing CakePHP Controllers the hard way

By now you already know or should know about CakeTestCase::testAction() and the wondrous things it can do. However, testAction has a few shortcomings. It can’t handle redirects, it doesn’t let you use the power of Mocks, and its impossible to make assertions on object state changes. Sometimes you need to do things the hard way, stick your fingers in the mud and work it out. However, knowing how to test a controller the old fashioned way takes a good knowledge of CakePHP.

Doing things the hard way

Controllers require a number of callbacks to be called before they are workable objects. So lets go, we’ll start with the venerable PostsController, and make a nice test for it.

Show Plain Text
  1. <?php
  2. App::import('Controller', 'Posts');
  3.  
  4. class TestPostsController extends PostsController {
  5.     var $name = 'Posts';
  6.  
  7.     var $autoRender = false;
  8.  
  9.     function redirect($url, $status = null, $exit = true) {
  10.         $this->redirectUrl = $url;
  11.     }
  12.  
  13.     function render($action = null, $layout = null, $file = null) {
  14.         $this->renderedAction = $action;
  15.     }
  16.  
  17.     function _stop($status = 0) {
  18.         $this->stopped = $status;
  19.     }
  20. }
  21.  
  22. class PostsControllerTestCase extends CakeTestCase {
  23.     var $fixtures = array('app.post', 'app.comment', 'app.posts_tag', 'app.tag');
  24.  
  25.     function startTest() {
  26.  
  27.     }
  28.  
  29.     function endTest() {
  30.  
  31.     }
  32. }
  33. ?>

So we start off with a basic test class, important things to notice are the fixtures array, and the test class. I’ve included all the fixtures that are related to the models my controller is going to use. This is important, as you will get tons of table errors until they are all setup.

You may have noticed that I created a subclass of the test subject, this lets me do a few things. First I can test functions that call redirect(), as they no longer redirect. I can also call methods that use $this->_stop() as they no longer halt script execution. Furthermore, I override Controller::render() so I can test actions that use render() without having to deal with piles of HTML. I personally don’t do many tests that assert the html of my views because I find it takes too much time and is tedious. Lastly, I set $autoRender to false just in case.

Show Plain Text
  1. function startTest() {
  2.     $this->Posts = new TestPostsController();
  3.     $this->Posts->constructClasses();
  4.     $this->Posts->Component->initialize($this->Posts);
  5. }
  6.  
  7. //tests are going to go here.
  8.  
  9. function endTest() {
  10.     unset($this->Posts);
  11.     ClassRegistry::flush();
  12. }

We then build the instance and call some basic callbacks, much like Daniel blogged about . At this point we have a controller instance and all the components and models built. We are now ready to start doing some testing.

Testing a controller method

Testing a controller method is just like testing any other method. Often there is a bit more setup involved as controllers are require more inputs by nature. However, it is all achievable in the test suite. So we are going to do a test of our admin_edit method. This admin_edit is straight out of bake, so you should know what it looks like. Furthermore, I can show how you can test methods that involve components like Auth.

Show Plain Text
  1. function testAdminEdit() {
  2.     $this->Posts->Session->write('Auth.User', array(
  3.         'id' => 1,
  4.         'username' => 'markstory',
  5.     ));
  6.     $this->Posts->data = array(
  7.         'Post' => array(
  8.             'id' => 2,
  9.             'title' => 'Best article Evar!',
  10.             'body' => 'some text',
  11.         ),
  12.         'Tag' => array(
  13.             'Tag' => array(1,2,3),
  14.         )
  15.     );
  16.      //more to come.
  17. }

At this point I’ve created the inputs I need for my controller action. I’ve got a session, and some test data. I’ve provided enough information in the session that AuthComponent will let me by and edit my records. However, many would say that you should bypass Auth entirely in your unit testing and just focus on the subject method. But being thorough never hurt.

Show Plain Text
  1. function testAdminEdit() {
  2.     $this->Posts->Session->write('Auth.User', array(
  3.         'id' => 1,
  4.         'username' => 'markstory',
  5.     ));
  6.     $this->Posts->data = array(
  7.         'Post' => array(
  8.             'id' => 2,
  9.             'title' => 'Best article Evar!',
  10.             'body' => 'some text',
  11.         ),
  12.         'Tag' => array(
  13.             'Tag' => array(1,2,3),
  14.         )
  15.     );
  16.     $this->Posts->params = Router::parse('/admin/posts/edit/2');
  17.     $this->Posts->beforeFilter();
  18.     $this->Posts->Component->startup($this->Posts);
  19.     $this->Posts->admin_edit();
  20. }

I’ve now simulated most of a request in CakePHP. It is important to fire the callbacks in the correct order. Just remember that beforeFilter happens before Component::startup(), and Component::beforeRender() happens after you call your controller action.

Making assertions

When I test controllers I usually make assertions on the viewVars that are set and any records that are modified / deleted. I don’t like making assertions on the contents of $this->Session->setFlash() as I find these messages change often which can lead to broken tests, which leads to frowns. Continuing from before:

Show Plain Text
  1. function testAdminEdit() {
  2.     $this->Posts->Session->write('Auth.User', array(
  3.         'id' => 1,
  4.         'username' => 'markstory',
  5.     ));
  6.     $this->Posts->data = array(
  7.         'Post' => array(
  8.             'id' => 2,
  9.             'title' => 'Best article Evar!',
  10.             'body' => 'some text',
  11.         ),
  12.         'Tag' => array(
  13.             'Tag' => array(1,2,3),
  14.         )
  15.     );
  16.     $this->Posts->params = Router::parse('/admin/posts/edit/2');
  17.     $this->Posts->beforeFilter();
  18.     $this->Posts->Component->startup($this->Posts);
  19.     $this->Posts->admin_edit();
  20.  
  21.     //assert the record was changed
  22.     $result = $this->Posts->Post->read(null, 2);
  23.     $this->assertEqual($result['Post']['title'], 'Best article Evar!');
  24.     $this->assertEqual($result['Post']['body'], 'some text');
  25.     $this->assertEqual(Set::extract('/Tag/id', $result), array(1,2,3));
  26.  
  27.     //assert that some sort of session flash was set.
  28.     $this->assertTrue($this->Posts->Session->check('Message.flash.message'));
  29.     $this->assertEqual($this->Posts->redirectUrl, array('action' => 'index'));
  30. }

So there you go a nice simple test for a controller, with redirects and session flashes. Since we are testing with the real session we should do the following to ensure there is no bleed through between tests.

Show Plain Text
  1. function endTest() {
  2.     $this->Posts->Session->destroy();
  3.     unset($this->Posts);
  4.     ClassRegistry::flush();
  5. }

By destroying the session we ensure that we have a clean slate on each test method. So that’s it really testing controllers really isn’t as hard as it may seem. There are some additional tricks that can be done with Mocks but that is another article all together.

Update Populated the controller params, so AuthComponent won’t complain.

Comments

Nice Mark! Every time people write about testing in Cake I get excited… until I try to do it myself. I just have to layout the starting point I guess.

primeminister on 12/18/08

Thanks for this Mark.
Just starting to work my way through it.

In startTest(), should $this->Posts->Component->initialize();
be
$this->Posts->Component->initialize($this->Posts); ?

Thanks

pragnatek on 12/18/08

pragnatek: You are correct, fixed it. Guess that’s what happens when you start writing at 11pm.

mark story on 12/18/08

Hi.. i’m newbie. How to run all of test things above? using CLI ? can u tell me how to run those tests?

Thx :)

polutan on 12/23/08

polutan: You need to setup simpleTest. Once simpletest is setup you can run unit tests from either the CLI or a web browser. Perhaps reading the section of the book related to getting unit testing setup would help there.

mark story on 12/24/08

Hey mark :) thank you very much. I will try..

polutan on 12/24/08

I am following the same pattern for my post controller with fixture. my test runs on live database instead of using $test. Here is my fixture :
class PostFixture extends CakeTestFixture { var $name = ‘Post’; var $import = array(‘table’ => ‘posts’, ‘records’ => false);
var $records = array( array (‘id’ => 1,‘user_id’ => 1, ‘name’ => ‘First Article’, ‘body’ => ‘First Article Body’, ‘status’ => ‘1’, ‘created’ => ’2007-03-18 10:39:23’, ‘modified’ => ’2007-03-18 10:41:31’), array (‘id’ => 2,‘user_id’ => 1, ‘name’ => ‘Second Article’, ‘body’ => ‘Second Article Body’, ‘status’ => ‘1’, ‘created’ => ’2007-03-18 10:41:23’, ‘modified’ => ’2007-03-18 10:43:31’), array (‘id’ => 3, ‘user_id’ => 1,‘name’ => ‘Third Article’, ‘body’ => ‘Third Article Body’, ‘status’ => ‘1’, ‘created’ => ’2007-03-18 10:43:23’, ‘modified’ => ’2007-03-18 10:45:31’) );
}

this is config/database.php
class DATABASE_CONFIG {

var $default = array( ‘driver’ => ‘mysql’, ‘persistent’ => false, ‘host’ => ‘localhost’, ‘login’ => ‘root’, ‘password’ => ‘’, ‘database’ => ‘cake12’, ‘prefix’ => ‘’, );

var $test = array( ‘driver’ => ‘mysql’, ‘persistent’ => false, ‘host’ => ‘localhost’, ‘login’ => ‘root’, ‘password’ => ‘’, ‘database’ => ‘test_cake12’, ‘prefix’ => ‘tes_suite_’, );
}

chirayu on 1/1/09

Does this technique still work with the fixtures as described in book.cakephp.org? Thanks for writing an illuminating article!

Rich Yumul on 1/12/09

Eh, please disregard my previous post. I just reviewed the article in a little more detail…

Rich Yumul on 1/12/09

Thanks a lot for this article.

I stopped testing part of my controllers functionality because I wanted to use fixtures and sessions, but now you gave me the key :-).

Javier on 1/14/09

Thanks for sharing this. I am excited to try this after several failed attempts to test controllers.

If I implement Cake’s ACL, does that mean I need to create fixtures for the ACL tables (aros, acos, and aros_acos)?

Hans on 1/27/09

Hi Mark thanks for this tutorial. This saved my day..
cheers

Dinesh on 4/29/09

Great Write Up! I’ve been scouring the net to get some more insight on how to test my CakeApps, and this was the ticket!

Robert Navarro on 5/2/09

Hi Mark. I’m heavily using this approach in my tests but I found that you could need to App::import(‘Model’) frist to get the controller instantiate the right class, at least in plugins.

I found that my plugins controllers tests failed beacause controller instantiated the model class as generic AppModel class.

Adding App::import(‘Plugin.Model’) did the trick.

Fran Iglesias on 7/5/09

Fran: An alternative is to set $this->plugin on the controller. This will let the controller know it is inside a plugin and load the correct model. See Controller::loadModel() for how this works.

mark story on 7/7/09

Thank you again Mark. I’ve just discovered it by myself after reading the controller.php core code.

But now, I’m aware that one must set all relevant params of the controller in order to test.

Fran Iglesias on 7/7/09

Thanks so much for this article. I’ve recently gotten excited about cake testing.

Just a thought, I know its probably outside protocol, but maybe you could post a link to this site from the official cakephp testing page? http://book.cakephp.org/view/366/Testing-controllers

under pitfalls it links to the ticket, but it might be more useful to link here?

Nico on 11/6/09

Thanks so much for this, I got it working (almost) except 1 tiny problem:

I was getting this error:

Unexpected PHP error [Undefined index: action] severity [E_NOTICE] in [\cake\cake\libs\controller\components\auth.php line 266]

On that very specific line of the auth.php we have:
$action = strtolower($controller->params[‘action’]);

I think the problem is obvious, ‘action’ was not set. But I wonder how should I go about fixing this? I thought this should be set automatically by cake?

raine on 12/27/09

Had the same problem as raine. I think it’s because we’re not using the Router so the params don’t get set for the url being ‘called’. At this point it gets complicated for those that have multiple admin sections with different prefixes and tests for them in the beforeFilter() of the controller.

Is there any way of calling urls using the test controller? maybe ‘/test_posts/index’ with testAction? (it doesn’t work with your setup since it’s not in the controllers folder… but putting it there would be a nice mess, mixing testing and real code…)

dave on 2/5/10

Hi,
to fix this error in your test(see raine comment):
Unexpected PHP error [Undefined index: action] severity [E_NOTICE] in [\cake\cake\libs\controller\components\auth.php line 266]

you could add :
$this->ControllerName->params[‘action’]=‘ControllerName’;
in function startCase(){….

Saludos

Francisco Quiñones on 2/13/10

Comments are not open at this time.