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?
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;
}
));
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));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With