Friday 6 September 2013

Dealing with date function in test under CakePHP

When writing test, we often run into situations like this:
        

    public function testReceived()
    {
        // ....
        $signed = true;
        // order is a mocked object
        $this->Order->expects($this->once())
            ->method('save')
            ->with(array(
                'signed' => $signed
                'received' => date('Y-m-d H:i:s')
            ));
        $this->Order->received($signed);
    }

    public function received($signed)
    {
        $this->save(array(
            'signed' => $signed,
            'received' => date('Y-m-d H:i:s')
        ));
    }

The test looks fine and it will possibly pass. Why possibly? The tricky thing here is the two calls of function 'date' and there is undoubtedly time difference between them. Thus, if you have a really slow machine or your first call to date function is not in the same second as the second one or you put a sleep function to sleep 1 second or more before calling 'received' method in the test, your test will be guaranteed to fail.

You may think using the second parameter which is $delta in PHPUnit's equalTo method to add tolerance of 1 or 2 seconds so that any time difference between 1 or 2 seconds will be regarded equal. However, 'equalTo' method's $delta parameter doesn't work on date string or date string in the array despite the wishful thinking.

The only solution left is to create our own constraint and assertion.
Go here and download the code then copy it into your CakePHP project.
In your test, you should add code below at the top:
        
    App::uses('AppTestCase', 'Lib/TestSuite');

Change your test class to extend from 'AppTestCase' instead of 'CakeTestCase'.
Finally, use '$this->isArrayEqual' with second parameter as tolerance of number of seconds and use '$this->assertArrayEquals' with third parameter as tolerance of number of seconds.

The test code above will become:
        

    public function testReceived()
    {
        // ....
        $signed = true;
        // order is a mocked object
        $this->Order->expects($this->once())
            ->method('save')
            ->with($this->isArrayEqual(array(
                'signed' => $signed
                'received' => date('Y-m-d H:i:s')
            ), 2)); // any difference of date string within 
                    // 2 second in the array will be regarded as equal
        $this->Order->received($signed);
    }

Now, you don't need to worry about these date strings causing problems anymore.