How to mock an Object Factory
Asked Answered
B

3

8

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?

Bailiwick answered 18/7, 2011 at 9:38 Comment(3)
Not sure what's wrong. Your code looks like it should "just work". You may also want to check out the abstract factory pattern. If it can be made to work in PHP (I don't know anything about polymorphism support in PHP), then you wouldn't have to code up expectations against a mock factory. You could just create and pass derived factory type instances that return mock foo/bar instances.Protostele
You could also see if your code would work with more direct Dependency Injection, instead. Rather than passing a factory, you'd just pass instances of foo and bar.Protostele
@Merlyn Definitely! If one knows at programming time what dependencies are needed passing those in directly is much easier/nicer. That way should only be used when the needed classes are not known at runtime or the factory is more complex (passing runtime paramters etc.). There is a great video on the subject: youtube.com/watch?v=RlfLCWKxHJ0Sigismond
S
9

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;
            }
       ));
Sigismond answered 18/7, 2011 at 9:55 Comment(0)
W
1

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));
Witching answered 18/7, 2011 at 18:36 Comment(7)
Shouldn't StubFactory extend Factory (type hinting)? And if you don't mind me asking: Why do you prefer this way over a mock like i described it? I would be interested in hearing your thoughts on that topic :)Sigismond
@Sigismond - Yes, good catch. I prefer this because it's easier to write each test that needs a different set of mocks returned by the factory: just pass them in as an array. There's no need to build complicated expectations for the factory.Witching
Wouldn't this also mean that you'd have to require the Factory class in your test code?Postdate
@dpk - Yes, if you're not using an autoloader. But if the object that receives the factory uses type-hinting, extending Factory will be a requirement.Witching
@DavidHarkness - That could be dangerous, as it could introduce classes to tests unexpectedly, changing it from a true "unit" test to something else. But at least directly requiring the file limits the risk somewhat. Still feels non-"unity".Postdate
@dpk - Your unit tests already depend on plenty of code outside itself that I'm comfortable using an autoloader. You should build your class modules so that loading them does not have any side effects. PHPUnit will end up loading them all as the tests progress unless you run your tests in process isolation.Witching
@DavidHarkness Fair enough. I don't run them in process isolation (way way too slow).Postdate
W
0

you should change your "business logic" ... i mean you don't have to pass Factory to the Sample constructor, you have to pass the exact parameters you need

Without answered 15/2, 2012 at 11:42 Comment(1)
thanks for the -1 but as Merlyn Morgan-Graham said: your sample class doesn't have to know about the factory, it should receive the exact object it needsWithout

© 2022 - 2024 — McMap. All rights reserved.