CakePHP 1.3 SimpleTest and Partial Mocks

Recently I was refactoring some old code we had built on CakePHP 1.3, and I was frustrated by a lack of easy error handling, specifically in the model function I was working with. After doing some digging, I determined that extending the ErrorHandler class, and creating my own error-types was the way to go.

For specifics on this, see: CakePHP 1.3 Error Handling. This allows you to stop execution and show your own error page, which is great for unrecoverable errors. It’s also much easier than bubbling all the way back to the controller, or somewhere else, to handle the error. I ran our unit tests which, with CakePHP 1.3, are using SimpleTest, and immediately saw the problem – when the code was hitting these new error cases, it was handling them like it would normally handle them – by redirecting to my custom error page. What to do?

Enter Partial Mocks

I found a Stack Overflow article describing the problem here, and introduced the concept of partial mocks. These are mock classes that duplicate the functionality of an existing class, but all functions return null and can take any number of arguments (read more on Mark Story’s blog post). From there, you can use various expect functions (full list at bottom of this page), instead of assertions, to expect (or not expect, etc.) a call to a specific function, like the cakeError function. I now knew what I needed to do, but based on the examples I could find, couldn’t get it to work properly. I was running into various errors; class not defined, Call to a member function dispatchMethod() on a non-object, etc. before finally combining a few articles (mainly Mark Story’s above, as well as this one, to solve the dispatchMethod() problem) into a proper solution. Here it is:

<?php
// I'm testing my Item model
App::import('Model', 'Item');

// as per the SO article, we are using a partial mock to override only the cakeError function
// all other functions stay the same
Mock::generatePartial('Item', 'MockItem', array('cakeError'));

class ItemTestCase extends CakeTestCase {
  var $fixtures = array(
    // all fixtures here
  );

  function startCase()
  {
    // create the Item instance, as per Cake documentation
    $this->Item =& ClassRegistry::init('Item');
  }

  function testRetrieveItemInfo()
  {
    // instantiate the MockItem
    $this->MockItem = new MockItem();
    // next two lines run the MockItem's __construct function, as this does not
    // run by default with mock classes, which caused errors for me
    $reflectionMethod = new ReflectionMethod('Item', '__construct');
    $reflectionMethod->invoke($this->MockItem);
  
    $this->MockItem->expectOnce('cakeError', array('calculationError', array('reason' => "Could not retrieve Item info for ID 5.")));

    $item = array('id' => 5, 'quantity' => 15);

    $this->MockItem->retrieveItemInfo($item);
  }

The first parameter to expectOnce is the function it should expect, and the second is an array of parameters the function should receive. Now, when you run the tests, it will report on this expectOnce call and whether or not it matched what you expected – no assert necessary.

Something to watch out for: normally, if execution were to hit the cakeError function, control would be redirected and stopped once the error page is shown. But remember, the mock class’s cakeError function doesn’t do anything, because it has been overridden, thus control is not redirected, and CakePHP continues through the function as if nothing happened. So, if you have something like this:

$itemInfo = $this->find('first', array('conditions'=>array('Item.id'=>$item['id'])));

// have an item id, but nothing was found in DB, throw error
if($itemInfo === false)
{
  $this->cakeError('calculationError', array('reason' => "Could not retrieve Item info for ID {$item['id']}."));
}

$item = array_merge($item, $itemInfo['Item']);
$item['item_id'] = $item['id'];
unset($item['id']);

normally you’d be fine, but when called with the mock class, the test will continue executing, so the array_merge line will run and it will bark that $itemInfo[‘Item’] doesn’t exist. I simply added the else condition to fix the problem:

$itemInfo = $this->find('first', array('conditions'=>array('Item.id'=>$item['id'])));

// have an item id, but nothing was found in DB, throw error
if($itemInfo === false)
{
  $this->cakeError('calculationError', array('reason' => "Could not retrieve Item info for ID {$item['id']}."));
}
else
{
  $item = array_merge($item, $itemInfo['Item']);
  $item['item_id'] = $item['id'];
  unset($item['id']);
}

which is probably better practice anyway, but just thought I’d give the heads up.

As a final caveat, I know this code works. Is it the best solution? Maybe not. If you know how to make it better, please let me know in the comments.

Your browser does not support SVG

Thanks for contacting us, we'll be in touch shortly!

Let's Talk Details.

A few questions to help us get the ball rolling