Как издеваться над Фабрикой Объектов - PullRequest
6 голосов
/ 18 июля 2011

Я использую фабрики (см. 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.

Так что, очевидно, невозможнопусть фиктивный объект возвращает разные значения в зависимости от переданных аргументов метода.

Как это можно сделать?

Ответы [ 3 ]

6 голосов
/ 18 июля 2011

Проблема в том, что 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 голос
/ 18 июля 2011

Создайте простой класс фабрики-заглушки, конструктор которого принимает экземпляры, которые он должен вернуть.

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));
0 голосов
/ 15 февраля 2012

Вы должны изменить свою "бизнес-логику" ... я имею в виду, что вам не нужно передавать Factory конструктору Sample, вы должны передавать точные параметры, которые вам нужны

...