Search code examples
phpphpunitfactory-pattern

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?


Solution

  • 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;
                }
           ));