Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock an Object Factory

I use Factories (see http://www.php.net/manual/en/language.oop5.patterns.php for the pattern) a lot to increase the testability of our code. A simple factory could look like this:

class Factory
{
    public function getInstanceFor($type)
    {
        switch ($type) {
            case 'foo':
                return new Foo();
            case 'bar':
                return new Bar();
        }
    }
}

Here is a sample class using that factory:

class Sample
{
    protected $_factory;

    public function __construct(Factory $factory)
    {
        $this->_factory = $factory;
    }

    public function doSomething()
    {
        $foo = $this->_factory->getInstanceFor('foo');
        $bar = $this->_factory->getInstanceFor('bar');
        /* more stuff done here */
        /* ... */
    }
}

Now for proper unit testing I need to mock the object that will return stubs for the classes, and that is where I got stuck. I thought it would be possible to do it like this:

class SampleTest extends PHPUnit_Framework_TestCase
{
    public function testAClassUsingObjectFactory()
    {
        $fooStub = $this->getMock('Foo');
        $barStub = $this->getMock('Bar');

        $factoryMock = $this->getMock('Factory');

        $factoryMock->expects($this->any())
            ->method('getInstanceFor')
            ->with('foo')
            ->will($this->returnValue($fooStub));

        $factoryMock->expects($this->any())
            ->method('getInstanceFor')
            ->with('bar')
            ->will($this->returnValue($barStub));
    }
}

But when I run the test, this is what I get:

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) SampleTest::testDoSomething
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-bar
+foo

FAILURES!
Tests: 1, Assertions: 0, Failures: 1.

So obviously it is not possible to let a mock object return different values depending on the passed method arguments this way.

How can this be done?

like image 451
Sebastian Heuer Avatar asked Jul 18 '11 09:07

Sebastian Heuer


2 Answers

The problem is that the PHPUnit Mocking doesn't allow you to do this:

$factoryMock->expects($this->any())
        ->method('getInstanceFor')
        ->with('foo')
        ->will($this->returnValue($fooStub));

$factoryMock->expects($this->any())
        ->method('getInstanceFor')
        ->with('bar')
        ->will($this->returnValue($barStub));

You can only have one expects per ->method();. It is not aware of the fact that the parameters to ->with() differ!

So you just overwrite the first ->expects() with the second one. It's how those assertions are implemented and it's not what one would expect. But there are workarounds.


You need to define one expects with both behaviors / return values!

See: Mock in PHPUnit - multiple configuration of the same method with different arguments

When adapting the example to your problem it could look like this:

$fooStub = $this->getMock('Foo');
$barStub = $this->getMock('Bar');

$factoryMock->expects($this->exactly(2))
       ->method('getInstanceFor')
       ->with($this->logicalOr(
                 $this->equalTo('foo'), 
                 $this->equalTo('bar')
        ))
       ->will($this->returnCallback(
            function($param) use ($fooStub, $barStub) {
                if($param == 'foo') return $fooStub;
                return $barStub;
            }
       ));
like image 119
edorian Avatar answered Oct 18 '22 01:10

edorian


Create a simple stub factory class whose constructor takes the instances it should return.

class StubFactory extends Factory
{
    private $items;

    public function __construct(array $items)
    {
        $this->items = $items;
    }

    public function getInstanceFor($type)
    {
        if (!isset($this->items[$type])) {
            throw new InvalidArgumentException("Object for $type not found.");
        }
        return $this->items[$type];
    }
}

You can reuse this class in any unit test.

class SampleTest extends PHPUnit_Framework_TestCase
{
    public function testAClassUsingObjectFactory()
    {
        $fooStub = $this->getMock('Foo');
        $barStub = $this->getMock('Bar');

        $factory = new StubFactory(array(
            'foo' => $fooStub,
            'bar' => $barStub,
        ));

        ...no need to set expectations on $factory...
    }
}

For completeness, if you don't mind writing brittle tests, you can use at($index) instead of any() in your original code. This will break if the system under test changes the order or number of calls to the factory, but it's easy to write.

$factoryMock->expects($this->at(0))
        ->method('getInstanceFor')
        ->with('foo')
        ->will($this->returnValue($fooStub));

$factoryMock->expects($this->at(1))
        ->method('getInstanceFor')
        ->with('bar')
        ->will($this->returnValue($barStub));
like image 36
David Harkness Avatar answered Oct 18 '22 02:10

David Harkness