Testing CakePHP Controllers the hard way

Dec 18 2008

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

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->beforeFilter();
  17.     $this->Posts->Component->startup($this->Posts);
  18.     $this->Posts->admin_edit();
  19. }

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->beforeFilter();
  17.     $this->Posts->Component->startup($this->Posts);
  18.     $this->Posts->admin_edit();
  19.  
  20.     //assert the record was changed
  21.     $result = $this->Posts->Post->read(null, 2);
  22.     $this->assertEqual($result['Post']['title'], 'Best article Evar!');
  23.     $this->assertEqual($result['Post']['body'], 'some text');
  24.     $this->assertEqual(Set::extract('/Tag/id', $result), array(1,2,3));
  25.  
  26.     //assert that some sort of session flash was set.
  27.     $this->assertTrue($this->Posts->Session->check('Message.flash.message'));
  28.     $this->assertEqual($this->Posts->redirectUrl, array('action' => 'index'));
  29. }

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.


Comments

primeminister on 18/12/08

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.

pragnatek on 18/12/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

mark story on 18/12/08

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

polutan on 23/12/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 :)

mark story on 24/12/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.

polutan on 24/12/08

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

chirayu on 1/1/09

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_’, );
}

Rich Yumul on 12/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 12/1/09

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

Javier on 14/1/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 :-).

Hans on 27/1/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)?

Dinesh on 29/4/09

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

Robert Navarro on 2/5/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!


Have Something to say?

*
* (Never published, I promise)
* You can use Textile markup, but be reasonable

Recent Artwork

  • Elephant shirt
  • Hammerhead Shark
  • CakeFest Berlin 2009 Badges
  • CakePHP test suite icons part 2
  • Whodunnit 2008 - Antelope
  • CakePHP Test suite icons

CakeFest Berlin

CakePHP CakeFest Berlin