如何模拟对象工厂

时间:2011-07-18 09:38:25

标签: php phpunit factory-pattern

我使用Factories(请参阅http://www.php.net/manual/en/language.oop5.patterns.php获取模式)来提高代码的可测试性。一个简单的工厂看起来像这样:

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

以下是使用该工厂的示例类:

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 */
        /* ... */
    }
}

现在进行正确的单元测试我需要模拟将返回类的存根的对象,这就是我遇到的问题。我认为可以这样做:

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

但是当我进行测试时,这就是我得到的:

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.

显然,不可能让mock对象根据传递的方法参数返回不同的值。

如何做到这一点?

3 个答案:

答案 0 :(得分:6)

问题是PHPUnit Mocking不允许你这样做:

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

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

expects只能有一个->method();。它不知道->with()的参数不同!

所以你只需用第二个覆盖第一个->expects()。这些断言是如何实现的,并不是人们所期望的。但是有一些解决方法。


您需要使用行为/返回值定义一个期望

请参阅:Mock in PHPUnit - multiple configuration of the same method with different arguments

在将示例调整为您的问题时,它可能如下所示:

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

答案 1 :(得分:1)

创建一个简单的存根工厂类,其构造函数接受应该返回的实例。

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];
    }
}

您可以在任何单元测试中重复使用此类。

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...
    }
}

为了完整起见,如果您不介意编写脆弱的测试,则可以在原始代码中使用at($index)代替any()如果被测系统更改了出厂调用的顺序或次数,这将会中断,但这很容易编写。

$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));

答案 2 :(得分:0)

你应该改变你的“业务逻辑”...我的意思是你不必将Factory传递给Sample构造函数,你必须传递你需要的确切参数