23

如何模拟以Iterator稳健方式实现接口的类的依赖项?

4

3 回答 3

24

网上已经有几个解决这个问题的现有解决方案,但我看到的所有解决方案都有一个类似的弱点:它们依赖于->expects($this->at(n)). PHPUnit 中的 'expects at' 函数有一些奇怪的行为,因为计数器是针对模拟的每个方法调用的。这意味着如果您在直接 foreach 之外对迭代器进行方法调用,则必须调整迭代器模拟。

解决方案是创建一个包含基本迭代器数据(源数组和位置)的对象并将其传递给returnCallback闭包。因为它是通过引用传递的,所以对象在调用之间保持最新,所以我们可以模拟每个方法来模拟一个简单的迭代器。现在我们可以正常使用迭代器模拟,而不必担心严格的调用顺序。

下面的示例方法可用于设置迭代器模拟:

/**
 * Setup methods required to mock an iterator
 *
 * @param PHPUnit_Framework_MockObject_MockObject $iteratorMock The mock to attach the iterator methods to
 * @param array $items The mock data we're going to use with the iterator
 * @return PHPUnit_Framework_MockObject_MockObject The iterator mock
 */
public function mockIterator(PHPUnit_Framework_MockObject_MockObject $iteratorMock, array $items)
{
    $iteratorData = new \stdClass();
    $iteratorData->array = $items;
    $iteratorData->position = 0;

    $iteratorMock->expects($this->any())
                 ->method('rewind')
                 ->will(
                     $this->returnCallback(
                         function() use ($iteratorData) {
                             $iteratorData->position = 0;
                         }
                     )
                 );

    $iteratorMock->expects($this->any())
                 ->method('current')
                 ->will(
                     $this->returnCallback(
                         function() use ($iteratorData) {
                             return $iteratorData->array[$iteratorData->position];
                         }
                     )
                 );

    $iteratorMock->expects($this->any())
                 ->method('key')
                 ->will(
                     $this->returnCallback(
                         function() use ($iteratorData) {
                             return $iteratorData->position;
                         }
                     )
                 );

    $iteratorMock->expects($this->any())
                 ->method('next')
                 ->will(
                     $this->returnCallback(
                         function() use ($iteratorData) {
                             $iteratorData->position++;
                         }
                     )
                 );

    $iteratorMock->expects($this->any())
                 ->method('valid')
                 ->will(
                     $this->returnCallback(
                         function() use ($iteratorData) {
                             return isset($iteratorData->array[$iteratorData->position]);
                         }
                     )
                 );

    $iteratorMock->expects($this->any())
                 ->method('count')
                 ->will(
                     $this->returnCallback(
                         function() use ($iteratorData) {
                             return sizeof($iteratorData->array);
                         }
                     )
                 );

    return $iteratorMock;
}
于 2013-04-09T16:19:41.737 回答
6

如果您只需要针对通用迭代器进行测试,那么 PHP(在 SPL 扩展中 - 在 PHP > 5.3 中无法关闭)内置了实现 Iterable 的数组包装器:SPL Iterators。例如

$mock_iterator = new \ArrayIterator($items);
$test_class->methodExpectingGenericIterator($mock_iterator);
$resulting_data = $mock_iterator->getArrayCopy();
于 2015-08-07T15:06:32.983 回答
5

这是一个结合了两全其美的解决方案,在ArrayIterator内部使用:

带有phpunit/phpunit模拟设施

/**
 * @return \PHPUnit_Framework_MockObject_MockObject|SomeIterator
 */
private function createSomeIteratorDouble(array $items = []): SomeIterator
{
    $someIterator = $this->createMock(SomeIterator::class);

    $iterator = new \ArrayIterator($items);

    $someIterator
        ->method('rewind')
        ->willReturnCallback(function () use ($iterator): void {
            $iterator->rewind();
        });

    $someIterator
        ->method('current')
        ->willReturnCallback(function () use ($iterator) {
            return $iterator->current();
        });

    $someIterator
        ->method('key')
        ->willReturnCallback(function () use ($iterator) {
            return $iterator->key();
        });

    $someIterator
        ->method('next')
        ->willReturnCallback(function () use ($iterator): void {
            $iterator->next();
        });

    $someIterator
        ->method('valid')
        ->willReturnCallback(function () use ($iterator): bool {
            return $iterator->valid();
        });

    return $someIterator;
}

phpspec/prophecy

/**
 * @return \PHPUnit_Framework_MockObject_MockObject|SomeIterator
 */
private function createSomeIteratorDouble(array $items = []): SomeIterator
{
    $someIterator = $this->prophesize(\ArrayIterator::class);

    $iterator = new \ArrayIterator($items);

    $someIterator
        ->rewind()
        ->will(function () use ($iterator): void {
            $iterator->rewind();
        });

    $someIterator
        ->current()
        ->will(function () use ($iterator) {
            return $iterator->current();
        });

    $someIterator
        ->key()
        ->will(function () use ($iterator) {
            return $iterator->key();
        });

    $someIterator
        ->next()
        ->will(function () use ($iterator): void {
            $iterator->next();
        });

    $someIterator
        ->valid()
        ->will(function () use ($iterator): bool {
            return $iterator->valid();
        });

    return $someIterator->reveal();
}
于 2015-09-06T10:32:32.460 回答