Code for Concinnity

beautiful and elegant solutions


Unit testing in CakePHP — the missing manual and a step-by-step tutorial

Today I’ve finally formalized a streamlined procedure to do unit testing properly in CakePHP — without all the pain. Needless to say, The Cookbook’s chapter on this issue cover the basic grounds but is inconsistent and lack a real life feeling to it.

The Cookbook’s coverage is really basic and doesn’t hold up to more real life complicated cases. Cake seems to be particularly picky about its automagical (too clever for me to figure out for a long time) configurations and bark errors at me very often. That led to reluctance to write tests and sometimes giving it up all together :p

The steps I’ve written here should lay down a very solid framework to make you bullet proof for all your future testing needs.

(Of course, this might be coming a bit late since most people would be better off looking at Cake’s successor Lithium, anyway here we goes.)

Can’t gather up much time to make a polished article but I believe this point-form brain dump is much more effective than most documentation out there.

Testing models

First, some notes

  • Testing models is the most important thing. Of course, a proper MVC application should have fat models and thin controllers.

  • For the most part, you really should use fixtures even if you do not use fixtures to load data. If you just use the database live, chances are Cake might mess up your data and errors might pop up, just go ahead and use fixtures. Yes, you don’t need to load data with your fixtures if you don’t want, but you need to turn this feature on. Yes, this was the thing that confused me a lot.

  • CakePHP uses SimpleTest, you would think that you’d put your setup and teardown code in setUp() and tearDown() just like everybody else? Wrong. Cake has already invaded those spaces. You’ll need to use startTest() and endTest(). This was documented in the Cookbook but this took me quite some time to figure out since I took it for granted and didn’t RTFM.

  • I may cover controller testing later but IMO controller testing in Cake is mostly broken and is quite straight forward to figure out.

Testing models — the tutorial

First we’ll set up the database, install SimpleTest etc. If you don’t know how to do this step you should read the Real Manual first.

Set up our testing application database in (My)SQL.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- We have the title field called "name" instead of "title"
-- IMO this is idiomatic Cake because your post's title
-- will now automatically show up in Post::find('list')
CREATE TABLE `posts`
(
    `id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    `name` VARCHAR(50) NOT NULL,
    `body` TEXT
);

-- You may think that the "name" field should be primary key.
-- Wrong! Doing so will make Cake unhappy if you ever use
-- Cake's console schema or the all other Cake migration
-- plugins out there. The id field is the Creed.
CREATE TABLE `tags`
(
    `id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    `name` VARCHAR(50) NOT NULL UNIQUE
);

-- The id field is the Creed, even for join tables
CREATE TABLE `posts_tags`
(
    `id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    `post_id` INT UNSIGNED NOT NULL,
    `tag_id` INT UNSIGNED NOT NULL,
    UNIQUE KEY(post_id, tag_id)
);

Bake our stuffs. Bake has come a long way since the earlier versions and the bake in Cake 1.3 is genuinely useful:

1
2
$ cake bake all Post
$ cake bake all Tag

Now go create some test data:

1
2
3
INSERT INTO `posts` (`id`, `name`) VALUES (1, "Lorem"), (2, "Ipsum"), (3, "Dolor"), (4, "Sit");
INSERT INTO `tags` (`id`, `name`) VALUES (1, "apple"), (2, "orange");
INSERT INTO `posts_tags` (`post_id`, `tag_id`) VALUES (1, 1), (1, 2), (2, 1), (3, 2);

Now open /app/models/post.php and /app/models/tag.php. Both look OK! And Cake has gone ahead and created the test cases for us at /app/tests/cases/model. Let’s try to run it

1
2
$ cake testsuite app all
Error: Missing database table 'test_suite_posts_tags' for model 'PostsTag'

This is how broken it is! Now let’s fix it. The thing is that we haven’t baked the fixture for the join table class PostsTag. Of course, we don’t want to have to create controller, models, views just for a simple join class. Luckily we can amend it by creating all missing fixtures:

1
$ cake bake fixture all -records -count 999

This will create all the missing fixtures. The -records -count 999 part tells Cake to pull real data from our database as fixture. The -count part is needed because it defaults to 10. Might as well enter some very large number but YMMV. This will also update our existing fixtures for Post and Tag so that the new fixture data we’ve added to the database will be reflected in the fixtures. Let’s try to run the tests again:

1
2
3
$ cake testsuite app all
...
4/4  test cases complete.

Finally, we’ve got the testing architecture down. To honor TDD, let’s create (edit) our test file before we do some development. Now go to /app/tests/cases/models/post.test.php, you’ll see this line:

1
var $fixtures = array('app.tag', 'app.post', 'app.posts_tag');

This is unfortunate, because it means every time you add a new association to your model, you’ll have to manually edit this $fixtures array. Too bad there isn’t an automated way to do this. (Running bake test will overwrite your whole file. You’ve been warned!)

Anyway, let’s just write some tests for kicks in /app/tests/cases/model/post.test.php:

1
2
3
4
5
6
7
8
function testSanity()
{
    $this->assertTrue(1 == 1);

    // Our fixtures should be loaded with data
    $posts = $this->Post->find('all');
    $this->assertTrue(!empty($posts));
}

Run it, all is good:

1
2
3
$ cake testsuite app all
...
4/4 test cases complete: 2 passes.

To honor TDD, we’ll write our test first before we do any implementation. Let’s add our test function. We’re going to write a model function that will find all posts for a given tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function testTagged()
{
    // These are based on our testing fixture data above
    $posts = $this->Post->tagged('apple');
    $this->assertTrue(!empty($posts));
    $this->assertTrue(Set::matches('/Post[name=Lorem]', $posts));
    $this->assertTrue(Set::matches('/Post[name=Ipsum]', $posts));
    $this->assertFalse(Set::matches('/Post[name=Dolor]', $posts));

    $posts = $this->Post->tagged('orange');
    $this->assertTrue(!empty($posts));
    $this->assertTrue(Set::matches('/Post[name=Lorem]', $posts));
    $this->assertTrue(Set::matches('/Post[name=Dolor]', $posts));
    $this->assertFalse(Set::matches('/Post[name=Ipsum]', $posts));
}

Run the test and watch it fail:

1
2
$ cake testsuite app all
...

Do our implementation in /app/models/post.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
 * If this line seems alien to you, don't worry.
 * Doing HABTM in CakePHP is a train wreck and whole another
 * topic. This is probably the most elegant way to do it
 * (not necessarily the shortest and cleanest) as far as
 * I know.
 *
 * The good thing about unit test -- we can be sure
 * it works even though we don't understand it lol
 */

function tagged($tag)
{
    $this->bindModel(array('hasOne'=>array(
        'PostsTag'=>array(
            'foreignKey'=>false,
            'conditions'=>"PostsTag.post_id = Post.id"
        ),

        'Tag'=>array(
            'foreignKey'=>false,
            'conditions'=>"PostsTag.tag_id = Tag.id"
        )
    )));

    return $this->find('all', array('conditions'=>array(
        'Tag.name'=>$tag)));
}

The above trick was used to force CakePHP to do left joins for us. There is an article that talks about this technique on the nuts and bolts of cakephp blog.

Well, let’s add one more test. We want to make a function to get us the content of a post:

1
2
-- First, update our test database
UPDATE `posts` SET `body` = "Hello World!!" WHERE `name` = "Lorem";
1
2
3
4
5
6
7
8
9
10
// /app/tests/cases/models/post.test.php
// Then write our new test
function testGetContent()
{
    $post = $this->Post->findByName("Lorem");
    $this->assertTrue(!empty($post));

    $content = $this->Post->getContent($post['Post']['id']);
    $this->assertPattern('/hello world/i', $content);
}

Run it, watch it fail. And then we add our implementation:

1
2
3
4
5
6
// /app/models/post.php
function getContent($id)
{
    $post = $this->read('body', $id);
    return $post['Post']['body'];
}

Great, now run it and expect it to pass….

1
2
3
$ cake testsuite app all
...
4/4 test cases complete: 11 passes, 1 fails.

Wtf!? It failed? Yeah, we forgot to update our fixtures. That’s it. Every time we update our database, we need to update the fixture. Fortunately, this is one area from Cake that is really painless:

1
2
3
4
5
$ cake bake fixture all -records -count 999
...
$ cake testsuite app all
...
4/4 test cases complete: 12 passes.

The fixture updating part was a little redundant, but it’s better than manually updating the fixture from SQL and also from *_fixture.php. I suggest you can store all your test fixtures data into an SQL file and make some bash script or ruby script to deconstruct the database and load the test SQL file for each test run. You can play around with different connection settings if you don’t want to sabotage your main table for every test run. You can package this loading of SQL file and the fixture baking into one script file so that it can be done in one click. (left as an exercise to reader)

That’s it! In retrospect, it isn’t rocket science, it’s just that I haven’t found any good piece of comprehensive tutorial that hand holds me from start to finish. I’ve been putting off a lot of unit test writing because I’ve always had Cake barking errors at me left and right. Now there’s no excuse :P

Published by kizzx2, on July 8th, 2010 at 12:53 am. Filled under: CakePHP Tags: , , , 2 Comments