Friday, March 05, 2010

Clever Mock Objects with PHPUnit

PHPUnit is the standard testing framework for php applications, and it has native support for mocking. It can produce various Stubs and Mocks by automatically extending a class or implementing an interface. Expectations can be set on these mocks so that assertions will be made on parameters and fake return values will be produced when particular methods are called (for a full explanation of mocks' utility see my free ebook.)
One thing I think PHPUnit lacks is support for multiple expectations. Let's fix ideas with a practicalfanciful example.

Imagine that you have a Telescope class (or interface) which defines an observeColor() method. When you call this method with the name of a planet or star, say Earth, Jupiter or Sun, it returns Blue, Grey, Yellow depending on what its lenses find out. I imagine these variables as plain strings to keep the discussion simple, but using ValueObjects do not change much the context.
The system under test is a class which composes Telescope as a collaborator. Given a list of objects in space, it does some calculations which involve their colors. Orienting a Telescope everytime you run the test suite is not practical, and the planets may not even be visible some times, thus you are forced to mock out Telescope (of course this situation is totally made up, but dependency management is a fundamental part of object-oriented design.)
To accomplish its responsibilities, the SUT needs to call observeColor() with a small set of different parameters and to obtain different results, depending on the particular test case. This is in fact a very common situation in testing.
As far as I know PHPUnit does not have a out-of-the-box support for defining multiple expectations on a mocked method without generating a conflict: if you set up expectations and constraint on method parameters, they must be satisfied in every single call to the mocked method. Similarly, if you set up a fake return value for the method, it will be hardcoded as it cannot depend on the input parameters.
I will present now a solution involving a callback anonymous function which works for a one-parameter mocked method. Only the mock setup and verification part is included, as the hypothetical SUT is not important in this explanation.
Note that anonymous functions are available only on php 5.3, so for previous versions of php you should create a small class with only one method, and pass array($object, 'methodName') as the callback.
<?php

class MultipleExpectationsTest extends PHPUnit_Framework_TestCase
{
    /**
     * This does not work. Multiple with() and will() calls
     * set up conflicting expectations and behaviors.
     */
    public function testMultipleExpectationsCanBePutOnTheSameMethodNatively()
    {
        $mock = $this->getMock('Telescope');
        $mock->expects($this->any())
             ->method('observeColor')
             ->with('Earth')
             ->will($this->returnValue('Blue'));
        $mock->expects($this->any())
             ->method('observeColor')
             ->with('Mars')
             ->will($this->returnValue('Red'));
        $this->assertEquals('Blue', $mock->observeColor('Earth'));
        $this->assertEquals('Red', $mock->observeColor('Mars'));
    }

    /**
     * We tune the expectation by passing it to a custom method.
     */
    public function testMultipleExpectationsCanBePutOnTheSameMethodViaACallback()
    {
        $inputs = array('Earth', 'Mars');
        $outputs = array('Blue', 'Red');
        $mock = $this->getMock('Telescope');
        $expectation = $mock->expects($this->exactly(2))
                            ->method('observeColor');
        $this->setMultipleMatching($expectation, $inputs, $outputs);
        $this->assertEquals('Blue', $mock->observeColor('Earth'));
        $this->assertEquals('Red', $mock->observeColor('Mars'));
    }

    /**
     * A callback is built and linked to the mocked method.
     */
    public function setMultipleMatching($expectation,
                                        array $inputs,
                                        array $outputs)
    {
        $testCase = $this;
        $callback = function() use ($inputs, $outputs, $testCase) {
            $args = func_get_args();
            $testCase->assertContains($args[0], $inputs);
            $index = array_search($args[0], $inputs);
            return $outputs[$index];
        };
        $expectation->will($this->returnCallback($callback));
    }
}

interface Telescope
{
    public function observeColor($name);
}
Update: in recent versions PHPUnit includes a $this->returnValueMap() option for will() that performs this job. Apparently it was inspired by this post.
 

6 comments:

beberlei said...

I am sorry to inform you that this is possible using $this->at(0), $this->at(1) instead of ->any(), ->once() and so on :-)

been there too ;)

Giorgio said...

I always wondered what the at() matcher meant. :)
However, if I understand its usage it's not the same thing. Isn't a bit brittle to couple the method calls order when a matching on parameters is needed? If you have N calls and you add one, up to N expectations breaks.
Usually in this cases I handroll a manual stub.

jakyra said...

You've got a lot of code to change if you refactor this and it's kind of hard to read and follow. I couldn't easily come in and figure out how this works or fix the test.

Instead, I like to build a method that does the assertion...

public function assertTelesopeColorMatchesPlanet($planet, $color) {
$mock = $this->getMock('Telescope');
$mock->expects($this->once())
->method('observeColor')
->with($planet)
->will($this->returnValue($color));
$this->assertEquals($color, $mock->observeColor($planet));
}

Which can then be called with clarity by
$this->assertTelesopeColorMatchesPlanet('Earth', 'Blue');
$this->assertTelesopeColorMatchesPlanet('Mars', 'Red');

This style of test writing keeps your tests small and easy to read.


This talk, http://www.youtube.com/user/GoogleTechTalks#p/search/9/Pq6LHFM4JvE , totally changed the way I do unit tests. I put the book on my must read list.

Giorgio said...

Wait, this is not my point. :)
The anonymous function hack is useful when the mock is injected somewhere (as all mocks should be) and the assertions are there only to demonstrate that the mock works. In tests you would only call setMultipleMatching() and then pass the mock to a class you want to test: it's the production code that will call the mocked method so you cannot wrap this call in the test suite.

Anonymous said...

@barberlei: as Giorgio has pointed out, using at() promotes very tight coupling - if the SUT adds any call to the object being mocked (or even changes the order of existing calls), the test will break even if the SUT itself is still working fine.

This leads to high maintenance costs, as any change to the way the SUT uses the mocked component can break a significant amount of tests.

The solution presented by Giorgio is elegant and does not suffer from this problem at all.

Great post by the way.

Sebastian Bergmann said...

https://github.com/sebastianbergmann/phpunit-mock-objects/issues/issue/31

ShareThis