Anatomy of a CakePHP Test Case

With all the talk of testing going on, I thought it would be good to look at how tests work and what is makes them tick. SimpleTest handles the bulk of test case execution, however, there are a few CakePHP specifics that are not part of a normal SimpleTest::UnitTestCase. By using CakeTestCase as the base class for your unit tests you get a few extra features such as Fixtures, Tag Assertion, and Controller Action testing, all of which make your tests more effective and easy to use.

Fixtures

Fixtures give you a way to insulate and isolate your tests, they allow for the creation of known data. By having known data you isolate your application’s test suite from the changes made to your application’s data. Fixtures are generated automatically if you bake your models too, making them easy to pick up and use.

Using fixtures in your tests

Show Plain Text
  1. <?php
  2. class MyTestCase extends CakeTestCase {
  3.     var $fixtures = array('app.post', 'app.comment');
  4.  
  5. }
  6.  
  7. ?>

The above would prepare the post and comment fixture for each of your test cases. The tables are truncated and re-inserted between each test case method as well, so you don’t have to worry about data bleeding through on your tests.

Tag Assertion

Tag Assertion is most helpful in regards to verifying helper output, or any generated HTML. Tag assertions streamline testing HTML output by replacing long complicated regular expressions with easier to understand arrays.

Show Plain Text
  1. <?php
  2.     $result = '<a href="/test.html">My link</a>';
  3.     $pattern = array(
  4.         'a' => array('href' => '/test.html'),
  5.         'My link',
  6.         '/a'
  7.     );
  8.     $this->assertTags($pattern, $result);
  9.  
  10.     $result = $form->input('Contact.email', array('id' => 'custom'));
  11.     $expected = array(
  12.         'div' => array('class' => 'input text'),
  13.         'label' => array('for' => 'custom'),
  14.         'Email',
  15.         '/label',
  16.         array('input' => array('type' => 'text', 'name' => 'data[Contact][email]', 'value' => '', 'id' => 'custom', 'maxlength' => 255)),
  17.         '/div'
  18.     );
  19.     $this->assertTags($result, $expected);
  20. ?>

The above gives a simple example of what can be done with assertTags(). Basically any snippet of HTML can be checked with assertTags(). The attributes in your pattern do not need to be in the same order as your text. However, all the text needs to case match the text you are checking. Also keep in mind that assertTags() will only work if your output’s attributes are double quoted, single quotes are not supported at this time.

Controller Action tests

Unit testing works fantastically for checking individual methods, and groups of methods. However, testing an entire request to your application is tricky. It can be a complicated process, setting the request parameters, constructing all the necessary classes, calling the action, and finally capturing the view output. This sounds like a lot of work and it is, enter testAction(). testAction() encapsulates a request and gives you a few options for checking the output. The return of testAction() can be View variables set by the controller, the return of the controller action, the bare rendered view, or an entire page. Furthermore, you can employ the same fixtures used in your model tests, or import existing table data into your test fixtures.

Show Plain Text
  1. <?php
  2.  
  3. $result = $this->testAction('/posts/index', array('return' => 'vars'));
  4.  
  5. ?>

There are several other options and configurations as well, but it is enough to fill up a separate post.

Test Method Execution order

Now that we’ve covered some of the functional differences CakeTestCase offers from UnitTestCase, we’ll take a look at what methods fire and when. CakeTestCase adds several methods to UnitTest and they are fired at specific times. By knowing when they run you can leverage them more effectively in your tests.

  • start Initializes the test, fixtures are created here.
  • startCase signals the beginning of a the test case.
  • testCaseMethods All of your test case methods fire here.
  • endCase signals the end of a test case.
  • end the end of the test, fixtures tables are dropped.

Furthermore, each and every of the above methods has before() and setUp() called before it. tearDown() and after() are called after each test. Start and After refresh your fixtures by inserting and truncating the tables and should only be overloaded if you need to change how the fixtures work. Both setUp() and tearDown() can be used to reset the state of your test case in between test case methods.

Comments

Hey Mark,

can you give an idea of how to test eg a controller::delete() method that has no view but just redirects to controller::referer() ?

controller::redirect() redirects out of the test but if i subclass the controller and avoid the redirect i get a missing view error.

Any tips would be greatly appreciated – thanks!

anonymous user on 10/20/08

JetPac: I would make TestPostsController class and override the redirect() method to set a property to the last redirected url. When subclassing make sure that you define viewPath to the old controller’s path, if you don’t you’ll get that missing view error. You can see examples of this in the core tests.

mark story on 10/24/08

Hi, Mark.

I’ve just tried your first test using assertTags (Cake 1.2.3) and it doesn’t pass:

Item #1 / regex #0 failed: Open a href=”/test.html”>My link tag

Unexpected PHP error [preg_match() expects parameter 2 to be string, array given] severity [E_WARNING] in [cake_test_case.php line 605]

I’ve been looking for more information about assertTags, but the closest thing I’ve found to what I’m trying to do is this very same post.

Is it a bug in Cake, is your example obsolete or am I just a bit dumb?

Thanks.

Javier on 6/10/09

Javier: Make sure that you are providing a string as the first parameter, giving an array will cause an error. The article is still accurate, and there are no current issues with assertTags() it is used 500+ times in the core test cases, you could look there for further examples.

mark story on 6/10/09

@Mark: OK, haven’t tried it yet, but I’ll do as you say and I’m absolutely sure you’re right.

But I’m afraid the article isn’t right:

$this->assertTags($pattern, $result);

Your first parameter is an array.

And, sure, I tried to look at the core test cases, but couldn’t find them at the API page nor at the main Cake website. Now I’ve noticed they’re at The Chaw (thanks for pointing out they’re still available to everyone).

Thanks!

Javier on 6/12/09

Mark,

Thanks for this – very helpful in getting me started on Testing.

Very confusing between all the different sources: cookbook, apress books, baking via console/libs/tasks/(test|model|controller).php.

All different and baking version doesn’t work with $fixtures since setUp() can’t instantiate “new {Name}sController” if u want to have fixtures – any associated models get loaded before the fixtures do!

I’ve totally modded my baking tasks to output your versions.

Question: Is there anyway to override tearDown() or setUp()/startCase() when working in groups?

I don’t necessarily want to TRUNCATE/CREATE/TRUNCATE, TRUNCATE/CREATE/TRUNCATE between TestCase files. I actually need/want incremental changes to test_suite_tables to be perpetuated through a series of Grouped tests.

Example: AccountsController does a bunch of stuff (tests) then, instead of reloading all the tables for UsersController to be tested, I’d rather build on what AccountsController tests already did instead of TRUNCATING then rebuilding everything – especially since I have 48 tables/fixtures.

Do you think I am just being daft?

regards,
oh4real

oh4real on 7/30/09

In case anyone is interested, I discovered the problem of tables getting truncated is even worse than I suspected, BUT I’ve “solved” my own problem with one simple addition to the cake_test_case.php script.

Worse: CakeTestCase::after(), which runs between test cases in a *.test.php actually truncates the tables. In this way, the db tables are truncated after every test method.

To preserve the tables, I have an insanely long testMyControllerMethod() – and it is not easy on the eyes for multi-step tests.

Solution:
I’ve added “ && $this->__truncated “ to the after() method’s first if() statement that calls for dropping the tables.

Then, when I want the database’s tables retained, between testMyControllerActionOne() and testMyControllerActionTwo(), I simply add $this->__truncated = false; as the last line of the former.

If I want the db’s tables truncated, I add $this->__truncated = true; – if necessary. The db tables will be truncated in after() and the subsequent test will create the tables.

It would be handy to have a way to keep a db intact during a GroupTest so that a db can be retained, but this just might be possible.

regards

oh4real on 8/2/09

Hey Mark, do you have any luck using $dropTables = false; in CaseTestCase? I don’t have any luck with that with 1.3 – the tables don’t get created at all. I want to use that because I have a lot of tables and dropping/recreating them takes a lot of time.

klevo on 12/17/09

klevo: No, I’ve never really used the dropTables property before. But doing a blame on the code shows it hasn’t changed many months, so I don’t know if its an issue specific to 1.3

mark story on 12/21/09

Comments are not open at this time.