Php 如何模拟对象工厂

Php 如何模拟对象工厂,php,phpunit,factory-pattern,Php,Phpunit,Factory Pattern,我经常使用工厂(请参见模式)来提高代码的可测试性。一个简单的工厂可能是这样的: class Factory { public function getInstanceFor($type) { switch ($type) { case 'foo': return new Foo(); case 'bar': return new Bar();

我经常使用工厂(请参见模式)来提高代码的可测试性。一个简单的工厂可能是这样的:

class Factory
{
    public function getInstanceFor($type)
    {
        switch ($type) {
            case 'foo':
                return new Foo();
            case 'bar':
                return new Bar();
        }
    }
}
$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;
            }
       ));
下面是使用该工厂的示例类:

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.
因此,显然不可能让模拟对象根据传递的方法参数以这种方式返回不同的值


如何做到这一点?

问题是PHPUnit mock不允许您这样做:

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

$factoryMock->expects($this->any())
        ->method('getInstanceFor')
        ->with('bar')
        ->will($this->returnValue($barStub));
每个
->method()只能有一个
方法。它不知道
->with()
的参数不同

因此,您只需用第二个覆盖第一个
->expects()
。这是这些断言的实现方式,而不是人们所期望的。但也有解决办法


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

见:

根据您的问题调整示例时,可能会如下所示:

class Factory
{
    public function getInstanceFor($type)
    {
        switch ($type) {
            case 'foo':
                return new Foo();
            case 'bar':
                return new Bar();
        }
    }
}
$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;
            }
       ));

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

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

你应该改变你的“业务逻辑”。。。我的意思是,您不必将工厂传递给示例构造函数,您必须传递所需的确切参数

不确定出了什么问题。您的代码看起来应该“正常工作”。您可能还想查看抽象工厂模式。如果它可以在PHP中工作(我对PHP中的多态性支持一无所知),那么您就不必根据模拟工厂编写期望值。您可以创建并传递返回mock foo/bar实例的派生工厂类型实例。您还可以查看您的代码是否可以使用更直接的依赖项注入。您只需传递
foo
bar
@Merlyn的实例,而不是传递工厂!如果在编程时知道需要哪些依赖项,那么直接将这些依赖项传递进来就容易多了。只有在运行时不知道所需的类或工厂更复杂(传递运行时参数等)时,才应使用这种方法。关于这个主题有一个很棒的视频:StubFactory不应该扩展工厂吗?如果你不介意我问你:为什么你喜欢这种方式而不是我描述的那种模仿?我很想听听你对这个话题的看法:)@edorian-是的,很好。我更喜欢这样,因为编写每个需要工厂返回的不同模拟集的测试更容易:只需将它们作为数组传入即可。没有必要为工厂建立复杂的期望值。这是否也意味着您必须在测试代码中要求工厂类?@dpk-是的,如果您没有使用自动加载器。但是,如果接收工厂的对象使用类型暗示,则需要扩展
工厂
。@DavidHarkness-这可能是危险的,因为它可能会意外地将类引入测试,将其从真正的“单元”测试更改为其他测试。但至少直接要求该文件在一定程度上限制了风险。仍然感觉不到“统一”。感谢-1,但正如Merlyn Morgan Graham所说:示例类不必了解工厂,它应该接收它所需要的确切对象