Flexible Mock Objects with Mockery

If you use stubs and mocks in your PHPUnit tests, you may have run across some of these limitations:

  • You want to call a mocked method more than once and have it expect or return different things on subsequent calls, e.g., multiple calls to query()
  • You want to set up an expectation that mocked methods are called in a certain order, e.g., validate() before save()
  • You want to mock a method that has an argument passed by reference, e.g., MongoCollection::insert()
  • You want to mock a method that doesn’t exist, i.e., the object being mocked only implements __call()

If you have, you may be pleased to know that there’s a drop-in replacement for PHPUnit’s mock objects that supports all of these use cases. It’s called Mockery.

Mockery is only a replacement for the mock object library in PHPUnit — it’s not a replacement for all of PHPUnit. You still still use PHPUnit to write and run your tests, but instead of calling $this->getMock() to get a PHPUnit mock object, you’ll call Mockery to get a Mockery mock object instead.

Installation

Installation is very simple. Just install Mockery via PEAR, set up autoloading, then call Mockery in your teardown() method:

require_once 'Mockery/Loader.php';
require_once 'Hamcrest/Hamcrest.php';
$loader = new \Mockery\Loader;
$loader->register();

class ExampleTest extends PHPUnit_Framework_TestCase
{
    public function teardown() {
        \Mockery::close();
    }
}

Mockery needs a hook at the end of each test so that it can verify any expectations you set up during the test.

There are a couple of different ways to install and integrate Mockery — check the documentation for more details.

Converting PHPUnit Mocks

Mockery uses slightly different syntax and method names than PHPUnit. Let’s consider this PHPUnit mock from my previous blog:

// create mock
$mock = $this->getMock('TwitterService');

// configure mock to expect a specific argument
// and to return a known set of tweets
$mock->expects($this->any())
     ->method('getTweetsBy')
     ->with($this->equalTo('jtai'))
     ->will($this->returnValue(array('a tweet')));

With Mockery, you’d instead write:

// create mock
$mock = \Mockery::mock('TwitterService');

// configure mock to expect a specific argument
// and to return a known set of tweets
$mock->shouldReceive('getTweetsBy')
     ->with('jtai')
     ->andReturn(array('a tweet'))
     ->zeroOrMoreTimes();

zeroOrMoreTimes() is actually the default behavior, so it can be omitted if you’d like.

Configuring Different Behavior on Subsequent Calls

Mocking a method that will be called multiple times during your test is no problem with Mockery. Just call shouldReceive() again to set up the subsequent expectations and/or return values.

$mock = \Mockery::mock('TwitterService');

$mock->shouldReceive('getLastTweetBy')
     ->with('jtai')
     ->andReturn('a tweet');

$mock->shouldReceive('getLastTweetBy')
     ->with('socalnick')
     ->andReturn('a reply');

echo $mock->getLastTweetBy('jtai');      // prints 'a tweet'
echo $mock->getLastTweetBy('socalnick'); // prints 'a reply'

You can even configure the same method called with the same arguments to return different values on subsequent calls by passing multiple values to andReturn():

$mock = \Mockery::mock('TwitterService');

$mock->shouldReceive('getRandomTweetBy')
     ->with('jtai')
     ->andReturn('Ate lunch', 'Funny cat picture');

echo $mock->getRandomTweetBy('jtai'); // prints 'Ate lunch'
echo $mock->getRandomTweetBy('jtai'); // prints 'Funny cat picture'

Subsequent calls to the same method will return the last configured value. Actually, this is true in general — when you configure andReturn() with only one value, it always returns the last (and only) value.

Enforcing Order in which Mocked Methods are Called

Suppose you want to ensure that an object’s validate() method is called before its save() method.

$mock = \Mockery::mock('Model');

$mock->shouldReceive('validate')
    ->once()
    ->ordered();

$mock->shouldReceive('save')
    ->once()
    ->ordered();

// test passes:
$mock->validate();
$mock->save();

// test fails (wrong order):
//$mock->save();
//$mock->validate();

// test fails (validate() not called):
//$mock->save();

// test fails (save() not called):
//$mock->validate();

Mocking Methods with Arguments Passed by Reference

Mockery uses reflection to determine if an argument is passed by reference, and you can manipulate an argument passed by reference by passing a closure to an expectation’s with() clause. This lets you mock classes like:

class Model
{
    function setLastModified(&$data)
    {
        $data['lastModified'] = date('c');
        return $this;
    }
}

Here’s how:

$mock = \Mockery::mock('Model');

$mock->shouldReceive('setLastModified')
     ->with(\Mockery::on(function(&$data) {
         $data['lastModified'] = '2012-02-12T15:19:21+00:00';
         return true; // indicate expectation matched
     }));

$values = array('foo' => 'bar');
$mock->setLastModified($values);
echo $values['lastModified']; // prints '2012-02-12T15:19:21+00:00'

The Mockery documentation has a more complete example that involves the PHP MongoDB driver; look for “Preserving Pass-By-Reference Method Parameter Behaviour”.

Mocking Non-existent Methods

Some classes use __call() to implement syntactic sugar. For example, this class allows you to call $model->findById(42) instead of $model->find('id', 42).

class Model
{
    public function __call($method, $args)
    {
        if (substr($method, 0, 6) == 'findBy') {
            $field = strtolower(substr($method, 6));
            $value = $args[0];
            $this->find($field, $value);
        }
    }

    public function find($field, $value)
    {
        // ... do the real work ...
    }
}

Mockery can mock methods like findById() that don’t actually exist. Just set up an expectation on the method as you would normally.

$mock = \Mockery::mock('Model');

$mock->shouldReceive('findById')
     ->with(42)
     ->andReturn(array('foo' => 'bar'));

$record = $mock->findById(42);
echo $record['foo']; // prints 'bar'

Conclusion

Hopefully these examples have given you an idea of what’s possible with Mockery. There are more features covered in the Mockery documentation that I didn’t cover here. If you find any neat use cases, let me know!

Tagged , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *