【问题标题】:How to mock an Object Factory如何模拟对象工厂
【发布时间】:2011-10-07 13:03:50
【问题描述】:

我经常使用工厂(请参阅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.

所以显然不可能让模拟对象以这种方式根据传递的方法参数返回不同的值。

如何做到这一点?

【问题讨论】:

  • 不确定出了什么问题。您的代码看起来应该“正常工作”。您可能还想查看抽象工厂模式。如果它可以在 PHP 中工作(我对 PHP 中的多态支持一无所知),那么您就不必针对模拟工厂编写期望。您可以创建并传递返回模拟 foo/bar 实例的派生工厂类型实例。
  • 您还可以查看您的代码是否可以使用更直接的依赖注入来代替。您只需传递 foo 和 bar 的实例,而不是传递工厂。
  • @Merlyn 绝对!如果在编程时知道需要哪些依赖项,直接将它们传递进来会更容易/更好。只有在运行时不知道所需的类或工厂更复杂(传递运行时参数等)时才应使用这种方式。关于这个主题有一个很棒的视频:youtube.com/watch?v=RlfLCWKxHJ0

标签: php phpunit factory-pattern


【解决方案1】:

问题是 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));

每个->method(); 只能有一个expects。它不知道->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;
            }
       ));

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 不应该StubFactory extend Factory(类型提示)?如果你不介意我问:为什么你更喜欢这种方式而不是像我描述的那样的模拟?我很想听听您对该主题的看法:)
    • @edorian - 是的,很好。我更喜欢这个,因为它更容易编写需要工厂返回的一组不同模拟的每个测试:只需将它们作为数组传递。无需为工厂建立复杂的期望。
    • 这是否也意味着您必须在测试代码中要求 Factory 类?
    • @dpk - 是的,如果您不使用自动加载器。但是如果接收工厂的对象使用类型提示,扩展Factory 将是一个要求。
    • @DavidHarkness - 这可能很危险,因为它可能会意外地将类引入测试,将其从真正的“单元”测试更改为其他东西。但至少直接要求文件在一定程度上限制了风险。仍然感觉不“团结”。
    【解决方案3】:

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

    【讨论】:

    • 感谢 -1,但正如 Merlyn Morgan-Graham 所说:您的示例类不必了解工厂,它应该收到它需要的确切对象
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-03-04
    • 1970-01-01
    • 2011-01-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-19
    相关资源
    最近更新 更多