Thursday 11 July 2013

Test Driven Design(TDD) in CakePHP 2 Part 1

I'd like to share the way how I implement TDD in CakePHP (2.0 and up) in this post by going through examples of tests for controller, model and other items found commonly used in CakePHP. I will be explaining why the test should be written as is and giving a few tips and tricks along the way.

You need to be an experienced CakePHP programmer to understand this post.

So, first things first, what is TDD?

You can find all the information about it around internet and I won't be repeating them. To me, TDD is three actions which repeat themselves:
  1. Write a test first
  2. Write code to make it pass
  3. Reduce duplication in the simplest and easiest way to make test stay passed 
Allow me explain these three steps in a simple example.

Write a test

Let's say we need a function called add, which returns sum of two numbers passed in. What we do is we write a test for it first.

function testAdd() {
    if (add(1, 2) === 3) {
        echo 'pass';
    } else {
        echo 'fail';
    }
}

If you run this function,  obviously, you will get a fatal error saying method is not defined which means the test has failed. Thus, we need to make it pass by doing step 2.

Write code to make it pass

Now, what is the code that can make this test pass?
The first thing come to my mind is this:
function add($number1, $number2) {
    return 3;
}
If you run the testAdd function again, you will see 'pass'. But hold on. This is not right, what we need is a function to sum any two number, not just for numbers adds up to three. Yes, you are absolutely right, hereby, we need step 3:

Reduce duplication in the simplest and easiest way to make test stay passed

Let's break this step into two part:
  1. find duplication
  2. reduce it
The duplication here is referencing the duplication between the test code and the code being tested. And clearly, the number 3 is the duplication here.

After finding this duplication, we need to reduce it in a simplest and easiest way. In the meantime, make the test stay passed. How are we going to do that, you asked? -- By changing the number 3 to $number1 + $number2.

So the code after this step will become:
function add($number1, $number2) {
    return $number1 + $number2;
}
I would like to stop here and talk about 'the simplest and easiest way' a little bit more.

When the duplication is reduced in the add function, it can go million ways, such as
function add($number1, $number2) {
    if ($number1 == 1.5 && $number2 == 1.5) {
        return $number1 * 6 - $number2 * 4;
    } else {
        return $number1 + $number2
    }
}
The function will still return the desired result, there is no more duplication and the test will stay passed. However, it is harder to maintain because of the extra logic. Thus, during duplication reduction process, we should write the simplest and easiest code to make the test stay passed.

Now you know the three steps of TDD. And you need to remember them and remind yourself every time you perform a TDD.

PHPUnit

Before going to the details of writing tests in CakePHP, we need to know something about PHPUnit because it is the test library sitting behind CakePHP's test environment.

Basically, it is a framework to help you write test. So things like
if (add(1, 2) === 3) {
    echo 'pass';
} else {
    echo 'fail';
}
becomes
class AddTest extends PHPUnit_Framework_TestCase {
    public function testAdd() {
        $this->assertEquals(3, add(1,2), "1+2 should equal to 3");
    }
}
PHPUnit has many method like this to make it easier to write test, check out the website: http://phpunit.de/manual/current/en/index.html and install it on your machine.

Don't worry if you are not familiar with PHPUnit. This dosen't give you difficulty of continuing reading the rest of this post and you can always go back to the link above to find out things you need to know.

TDD in CakePHP

Finally, we are here. Let's start with a simple example again.

We need to build a page shows a list of cakes. And according to the design we got from our designer we have written a view with html and some php code to iterate a view variable named 'cakes'. We also have a table called cakes with fields 'id', 'name', 'created' and 'updated'. I assume that you have baked empty controller and model for this table in the console so that we don't need to create these files manually.

Apparentally , we need an 'index' action in the CakesController for this task. Do we just start writing code of it? No! we start with a test for it like the one below:
// in the CakesControllerTest.php under app/Test/Case/Controller
App::uses('CakesController', 'Controller');

/**
 * CakesController Test Case
 *
 */
class CakesControllerTest extends ControllerTestCase {

/**
 * testIndex method
 *
 * @return void
 */
    public function testIndex() {
        $this->testAction('/cakes', array(
            'method' => 'GET'
        ));
    }

}
This test can be run in the terminal by typing 'Console/cake test app Controller/CakesController' under app folder. When the test runs, something will happen behind the scene as if the url of '/cakes' is visited with a http GET request and this is because testAction method is called in this test. Then, en error will be incurred since the action 'index' matches the url '/cakes' does not exist yet.
To make the test pass, which is the second step of TDD, a method of 'index' sitting in the 'CakesController' will be perfect.
// in the CakesController.php 
public function index() {

}
If you run the test again, it will pass('OK (1 test, 0 assertions)' with green background if you are not under windows). And there is no need for step3 here, because there is no duplication and the code is simple and easy enough.

Now, as said before TDD is the repetition of those three steps, we need to write some more code in the test to ensure the list of cakes is retrieved and set as a view variable. To do that, we are basically testing the find method on the Cake model gets called with parameter 'all'. The question here is how we are going to test that a particular method(in this case 'find') is invoked and the parameter passed to the method is correct. The answer is to use 'generate' and 'expects' methods like below:
// in the CakesControllerTest.php under app/Test/Case/Controller
/**
 * test index method
 *
 * @return void
 */
public function testIndex()
{
    $this->generate('Cakes', array(
        'models' => array(
            'Cake' => array(
                'find'     
            )
        )
    ));   
 
    $this->controller->Cake->expects($this->once())
        ->method('find')
        ->with('all');

    $this->testAction('/cakes', array(
        'method' => 'GET'
    ));
}
After calling generate method like this, model 'Cake' will become a fake 'Cake' object (we call it mocked object) with a fake 'find' method (mocked method) and 'CakesController' is assigned to '$this->controller'. Then, you can expect the 'find' method is called once on 'Cake' model with parameter 'all', which is the three lines of code after.

Now, if you run the test again, the test will fail and tell you that 'find' method is expected to be called once but actually called zero times. The message is clear enough to tell us that we need to call 'find' method in the index method as below:
// in the CakesController.php 
public function index()
{
    $this->Cake->find('all');
}
Run the test again, you will see it pass. It is time to go to step1 again. To display the list, we need to make sure a variable 'named' cakes is set on the controller.
// in the CakesControllerTest.php under app/Test/Case/Controller
/**
 * test index method
 *
 * @return void
 */
public function testIndex()
{
    $this->generate('Cakes', array(
        'models' => array(
            'Cake' => array(
                'find'     
            )
        )
    ));

    $fakeCakes = 'some cakes';

    $this->controller->Cake->expects($this->once())
        ->method('find')
        ->with('all')
        ->will($this->returnValue($fakeCakes));

    $this->testAction('/cakes', array(
        'method' => 'GET'
    ));

    $this->assertEquals($fakeCakes, $this->vars['cakes']);
}
The line of calling method 'will' means when test runs the 'find' method in the controller will return a string 'some cakes'. Then, the last line is to make sure the view variable set in the 'index' method has a name of 'cakes' and value of 'some cakes'. You may be wondering why the 'vars' on the test class is the view variables on the 'CakesController' class. This is because after 'testAction' method finish running the code in the 'CakesController', it saves the view variables on the test class object. So $this->vars in the test is actually view vars from 'CakesController'. Another thing needs some attention here is the value of $fakeCakes. If we change it to some other string or even an array or an object, the test still works because it is just to ensure that what comes out of 'find' method is the same as what is in the view variables. The code in the 'CakesController' to make the test pass is:
// in the CakesController.php 
public function index()
{
    $this->Cake->find('all');
    $this->set('cakes', 'some cakes');
}
If you run the test now, it will pass. But this code is not right. And how can we correct it? Easy! We do step3 'Reduce duplication in the simplest and easiest way to make test stay passed'. As we can see, the duplications between test code and controller code are the call of method 'find' with parameter 'all', string 'cakes' and string 'some cakes'. The method call can not be possibly reduced, so it stays there; the string 'cakes' can not be reduced either since it needs to be the name of the view variable. That leaves us the string 'some cakes'. Because the return value of 'find' method is expected to be 'some cakes', which is the same we put as the second parameter for 'set' method, the duplication can be reduced by passing the return value of 'find' method to the place that 'some cakes' is and without much thinking this is the simplest and easily way to reduce duplication.
// in the CakesController.php 
public function index()
{
    $this->set('cakes', $this->Cake->find('all'));
}
If you run the test again, it will stay passed. Since there is no more test to write for this functionality, it just simply indicates that there is no more code needed in the 'index' method.

As time goes by, new design comes along. This time in the 'index' method we created, we need to limit the number of cakes displayed to 5 if user is not logged in. Otherwise, limit them to 10. And we don't need the created date anymore in the view.
To fulfill the task above, we are going to change the test. First, we are going to test the situation that user has not logged in.
// in the CakesControllerTest.php
public function testIndexRetrievesFiveCakesWhenUserNotLoggedIn() {
    $this->generate('Cakes', array(
        'models' => array(
            'Cake' => array(
                'find'     
            )
        ),
        'components' => array(
            'Auth' => array(
                'user',
                'startup',
                'shutdown'
            )
        )
    ));

    $this->controller->Auth->staticExpects($this->once())
        ->method('user')
        ->with('id')
        ->will($this->returnValue(null));

    $fakeCakes = 'some cakes';

    $this->controller->Cake->expects($this->once())
        ->method('find')
        ->with('all', array(
            'fields' => array(
                'Cake.id',
                'Cake.name',
                'Cake.modified'
            ),
            'limit' => 5
        ))
        ->will($this->returnValue($fakeCakes));

    $this->testAction('cakes', array(
        'method' => 'GET'
    ));
    $this->assertEquals($fakeCakes, $this->vars['cakes']);
}
First, we changed the method name to indicate what it is testing. In addition, 'components' array has been added to 'generate' method call. This is to mock up 'user' method on the 'Auth' component so that during the test if there is any code in the 'index' action calls this method, it will not return the value in the session, instead it is going to return the value we set in the test, which has been set up by the code after the 'generate' method call. You may find out, there is a slight difference here, which is the call to 'staticExpects' method. Because 'user' method is a static method on AuthComponent, 'expects' method here will not work, instead we use 'staticExpects'. Of course you may say that we can use 'loggedIn' method instead of 'user'. I fully agree to that, but that method doesn't give me the chance of explain the usage of 'staticExpects'.

As to the 'startup' and 'shutdown' being here is because that 'user' method is called in these two methods during the request cycle and we don't want them to interfere the assertion that 'user' method on the AuthComponent only being called once in the 'index' method(this is needed for cakephp 2.3.6 and up).

Let us stop here for a moment and think about the meaning of this test. When the test run, 'index' method in the CakesController will be hit; 'user' method on 'Auth' component needs to be called once, its parameter will be 'id' and then return 'null'; 'find' method on the model 'Cake' also needs to be called,  its parameter will no longer be just 'all' but with the same second parameter we pass to 'with' method in the test and return value 'some cakes'. After the execution of controller code, we assert that view variable 'cake' has been set on the 'CakesController' with value 'some cakes', which is the same coming out of 'find' method.

The code to let this test pass is:
// in the CakesController.php 
    public function index() {
        $this->Auth->user('id');
        $cakes = $this->Cake->find('all', array(
            'fields' => array(
                'Cake.id',
                'Cake.name',
                'Cake.modified'
            ),
            'limit' => 5
        ));
        $this->set('cakes', $cakes);
    }
All the duplication can not be reduced further.
Then, we start another test to make sure when 'user' method returns not null value, limit will be set to 10 instead of 5.
// in the CakesControllerTest.php
    public function testIndexRetrievesTenCakesWhenUserLoggedIn()
    {
        $this->generate('Cakes', array(
            'models' => array(
                'Cake' => array(
                    'find'     
                )
            ),
            'components' => array(
                'Auth' => array(
                    'user',
                    'startup',
                    'shutdown'
                )
            )
        ));

        $this->controller->Auth->staticExpects($this->once())
            ->method('user')
            ->with('id')
            ->will($this->returnValue(1));

        $this->controller->Cake->expects($this->once())
            ->method('find')
            ->with('all', array(
                'fields' => array(
                    'Cake.id',
                    'Cake.name',
                    'Cake.modified'
                ),
                'limit' => 10
            ));

        $this->testAction('/cakes', array(
            'method' => 'GET'
        ));
    }
The difference between this test and the test before is that now 'user' method returns a value 1 and 'find' method has 'limit' of 10. And to make this more clear we can organize these two tests in the following way:
// in the CakesControllerTest.php
/**
 * index method shared by 
 * testIndexRetrievesFiveCakesWhenUserNotLoggedIn and 
 * testIndexRetrievesTenCakesWhenUserLoggedIn
 *
 *
 * @param integer $userId
 * @param integer $limit
 * @return void
 */
    private function index($userId, $limit) {
        $this->generate('Cakes', array(
            'models' => array(
                'Cake' => array(
                    'find'     
                )
            ),
            'components' => array(
                'Auth' => array(
                    'user',
                    'startup',
                    'shutdown'
                )
            )
        ));

        $this->controller->Auth->staticExpects($this->once())
            ->method('user')
            ->with('id')
            ->will($this->returnValue($userId));

        $fakeCakes = 'some cakes';

        $this->controller->Cake->expects($this->once())
            ->method('find')
            ->with('all', array(
                'fields' => array(
                    'Cake.id',
                    'Cake.name',
                    'Cake.modified'
                ),
                'limit' => $limit
            ))
            ->will($this->returnValue($fakeCakes));

        $this->testAction('/cakes', array(
            'method' => 'GET'
        ));
        $this->assertEquals($fakeCakes, $this->vars['cakes']);
    }
/**
 * testIndex method
 *
 * @return void
 */
    public function testIndex() {
        $this->index(null, 5);
    }

/**
 * test index method with logged in user
 *
 * @return void
 */
    public function testIndexWithUserLoggedIn()
    {
        $this->index(1, 10);
    }
The reason of doing this is that, by keep the code shared by the two tests, it is easier to maintain the test code. Also, it is more clear that the user id coming out of 'Auth->user' has a direct impact on the limit. So, the code let these two tests pass is:
// in the CakesController.php 
    public function index() {
        $cakes = $this->Cake->find('all', array(
            'fields' => array(
                'Cake.id',
                'Cake.name',
                'Cake.modified'
            ),
            'limit' => $this->Auth->user('id') ? 10 : 5
        ));
        $this->set('cakes', $cakes);
    }
When you run the test, it will pass. Again, the duplication between the test and the code can not be reduced.

So far, we have used tests for controllers to ensure the functional structure of the controller code. However, we are still not sure the code we have written in the controller to let these tests pass is 100% correct without open a browser and navigate to 'you_local_domain/cakes'. For example, if we change the line "->method('user')" to "->method('users')" and calling 'users' instead of 'user' on Auth component in the 'index' method, the test will still pass but when the real request comes in, the application will crash with error.

Of course, we can not guarantee the test we wrote is 100% correct. That's why we require a certain level of familiarity of the framework before you can start on test driven design. Plus, there are some tricks to reduce the possibility of composing an incorrect test. Putting code shared by tests testing same action into one private method like the last example is one. Reducing the code we put in the test is another one I am going to talk about.

To make a mistake in writing code, you need to start writing code first. Therefore, reducing the code you write is a way to reduce the possibility of making mistakes in writing code. Still using the tests we wrote above as an example, we can change the private 'index' method as followed:
    private function index($userId, $limit) {
        $this->generate('Cakes', array(
            'models' => array(
                'Cake' => array(
                    'getNumberOfCakes'     
                )
            ),
            'components' => array(
                'Auth' => array(
                    'user',
                    'startup',
                    'shutdown'
                )
            )
        ));

        $this->controller->Auth->staticExpects($this->once())
            ->method('user')
            ->with('id')
            ->will($this->returnValue($userId));

        $fakeCakes = 'some cakes';

        $this->controller->Cake->expects($this->once())
            ->method('getNumberOfCakes')
            ->with($limit)
            ->will($this->returnValue($fakeCakes));

        $this->testAction('/cakes', array(
            'method' => 'GET'
        ));
        $this->assertEquals($fakeCakes, $this->vars['cakes']);
    }
This test uses a method 'getNumberOfCakes' which takes $limit as parameter to replace 'find' method, which reduce the amount the code of specifying what fields needs to be retrieved and what type of 'find' method needs to be called and therefore leaves us less chance to make a mistake here. The code to let this test pass is:
    public function index() {
        $cakes = $this->Cake->getNumberOfCakes(
            $this->Auth->user('id') ? 10 : 5
        );
        $this->set('cakes', $cakes);
    }

Again, the duplication here can not be reduced. Now, we only need to ensure that method 'getNumberOfCakes' returns the correct results and that can be done in the tests of 'Cake' model. Whereas before, we have no way to ensure the results coming out of 'find' is really what we need since 'find' method is a CakePHP method and writing a test for it will not be test driven design. Also the code in the controller is less then before, which conforms to the 'fat model slim controller' principal in MVC pattern. This is a typical example of reducing code in controller tests leading to a slim controller.

Up to now, we have learned the basics of writing test for controller. And I am going to write a few tips here to help you solve some problem further down the way.

Exceptions

Some time you wanna assert that under some situation an action will throw out an exception. This can be done by adding an annotation to documentation before method declaration.
/**
 * @expectedException MethodNotAllowedException
 */
public function testIndex()
{
    $this->testAction('/cakes/');
}
This code means 'index' method is supposed to throw out an 'MethodNotAllowed' exception. The extensive usage can be found here.

Use most specific assertions

As the code going more and more complicated, you will find it is harder to test only use $this->assertEquals(). You can go here to find all kinds of assertion method to suit your needs.

Test certainty out of randomness

If your test involving a limited randomness, you need to test the certainty out of the randomness. Let's assume you want to test a method named 'getNumberOfCakes' on Cake model to take a random number between 10 and 100 in your action. The randomness is obviously the integer passed to 'getNumberOfCakes' and the certainty is that this number is between 10 and 100. So you can write:
// in the test method of test class extending ControllerTestcase
$this->controller->Cake->expects($this->once())
    ->method('getNumberOfCakes')
    ->with($this->logicalAnd(
        $this->greaterThan(10), 
        $this->smallerThan(100)
    ));
This code puts constrains of larger than 10 and smaller than 100 on the first parameter passed to 'findNumberOfCakes'.

Expects the times that methods gets called

In the example before, we use $this->once() to assert that method needs to be called only once. You can also use $this->any(), $this->never(), $this->atLeastOnce() and $this->exactly to suit your purpose. In addition, you can use $this->at($index) to assert the first, second or third... time a method to be called. You can use $this->onConsecutiveCall($value1, $value2, $value3 ....) in the $this->will to let a method return a series of results.

Always mock up beforeFilter

// in test methods of test classes extending ControllerTestCase 
       $this->generate('Cakes', array(
            'methods' => array(
                'beforeFilter'
            )
       ));
Add 'methods' key to 'generate' method's second parameter will give you the ability to obtain a mocked method on the controller. When tests run, the real 'beforeFilter' method will no longer be called. This makes your test more focused on the code in the action instead of repeating the test of what is going on in the 'beforeFilter' for every actions in the controller. For instance, you want to add some code to the 'beforeFilter' method of 'AppController' which involves calling method 'user' on the 'Auth' component. After you do that, you will find the test for 'index' method in 'CakesControllerTest' become failed because we only expect 'user' method to be called once on 'Auth' component. Some will say that we can change '->once()' to '->at(1)'. But imagine you have written 50 tests like this, and one more line of calling '$this->Auth->user' in the 'beforeFilter' will make you change '->at(1)' to '->at(2)' all over the tests, which means mocking up 'beforeFilter' leads to better maintainability of tests.

$this->getMock() and $this->getMockForModel()

Sometimes, you need a mocked object which can not be obtained by calling '$this->generate()' method. You can turn to using 'getMock' method. It is a very powerful method but really simple to use by passing the class name you want to mock up and the methods you want to mock up to it as the first and the second parameters. So we can do something like this:
public function testSomeMethod()
{
    $this->generate('Cakes', array(
        'methods' => array(
            'getEventManager'
        )
    ));
    $EventManager = $this->getMock('CakeEventManager', array(
        'dispatch' 
    ));
    $this->controller->expects($this->any())
        ->method('getEventManager')
        ->will($this->returnValue($EventManager));

    $EventManager->expects($this->once())
        ->method('dispatch')
        ->with($this->isInstanceOf('CakeEvent'));
    $this->testAction('/cakes/some_method');
}
This is to expect that 'getEventManager' gets called and returns a mocked object of 'CakeEventManager' with mocked method 'dispatch' and then we can use the object to expect that 'dispatch' method gets called with instance of 'CakeEvent'.
As to 'getMockForModel' method, it is a wrapper method of 'getMock' in order to pass configuration as the third parameter to 'getMock' to instantiate a mock model object and then put it in the 'ClassRegistry' so that next time you use it through '$this->loadModel()' in controller or 'ClassRegistry::init()' anywhere else you get a mocked model object. To use it, just simply call it with model name and an array of methods you want to mock up and it will return a mocked model object. Then you can call 'expects' method on it like we do with other mocked object.

The request object on $this->controller

'$this->controller->request' is a mocked object. However, if you call 'expects' on it, you will not get desired result or behavior even if you get a mocked 'CakeRequest' using 'getMock' and assign it to '$this->controller'. This may sound confusing. So let me explain it. When you call 'testAction' in the test method, an object of 'CakeRequest' will be instantiated and assigned to '$this->controller'. So before calling 'testAction' method, '$this->controller->request' is not a mocked object. And if you assign a mocked 'CakeRequest' to it and do '$this->controller->request->expects(...)...', it will not work since the '$this->controller->request' will be replaced with a new object in the 'testAction'. Because 'expects' must be called on mocked object before 'testAction' trigger the test, there is just not a simple way of calling 'expects' on '$this->controller->request'.

Simulating AJAX request

Now we know we can't do something like '$this->controller->request->expects...' to expect 'is' method is called on request object with 'ajax' as parameter and returns 'true'. So how do we simulating a ajax request? Easy! Just set $_SERVER['HTTP_X_REQUESTED_WITH'] to 'XMLHttpRequest' before calling 'testAction' and don't forget to unset it after 'testAction'.

The sequence of models passed to generate 

When action gets complicated, you may want more than one mocked model through 'generate' method. And it is very important that you put them in right order. Let's say you want to mock up two models named 'Account' and 'User'. If you put them under 'models' key to 'generate' method in the order of 'Account' and 'User', what happens first inside 'generate' method is to get a mocked object of 'Account', which triggers the '__construct' method on the 'Model' class to go through all the relationship set up in 'Account' model and create object of related models onto itself(if they are not in the class registry). Thus, if you have a relationship set up on 'Account' says 'hasMany' 'User', you will have a 'User' object sitting on 'Account' model after this point. Then the 'generate' method moves to the next model you passed to it, which is 'User' and put the mocked object of it in the class registry. Finally, when you want to do '$this->controller->Account->User->expects...' in your test, you will get an error since the 'User' here is not the one in the class registry that 'generate' method puts in but a real 'User' object created from '__contract' method of 'Model'.

Run test individually

Some times, running all tests in one controller can take some time. There is a way to only run some of them. Using '--filter' like this 'Console/cake test app controller/UsersController --filter testIndex' runs tests in the 'UsersController' starting with 'testIndex'. So if you have a test named 'testIndexThrowException', it will run as well.

--stderr option

 If you use 'CakeSession' in your test, you will have some trouble. Appending '--stderr' in the command line will solve this problem.


Thank you for reading this post and I will be adding more tricks and tips here to help you write test for controllers. Comments and criticism are welcomed.

In the meantime, stay tuned for the next part of this post about test for models.