如何使用PHPUnit模拟实现Iterator接口的类?

Dan*_*Dan 19 php phpunit iterator mocking

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

Dan*_*Dan 21

已经有几个现有的解决方案已经解决了这个问题,但我见过的所有解决方案都有类似的弱点:他们依赖->expects($this->at(n)).PHPUnit中的'expected 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;
}
Run Code Online (Sandbox Code Playgroud)

  • 我正在发布我遇到的问题的解决方案.由于我没有博客,我把它放在这里.我认为这是一种使用SO的有效方法,因为当你发布一个问题时,有一个复选框可以做到这一点:) (12认同)
  • 回到这个问题(因为我注意到我不断从中获得积分),在我的带领下经历了几年的单元测试经验,我现在问你是否真的需要模拟那个自定义迭代器.值得思考的是,你将获得多少好处,以及通过模拟东西,你将为测试增加多少复杂性.我想在两年前给我一个怀疑的好处,并假设我有充分的理由进行这种模拟,但我有一种感觉,真的应该只是使用真正的课程. (4认同)

loc*_*inz 6

这是一个结合了两全其美的解决方案,使用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;
}
Run Code Online (Sandbox Code Playgroud)

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();
}
Run Code Online (Sandbox Code Playgroud)


Jef*_*ton 5

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

$mock_iterator = new \ArrayIterator($items);
$test_class->methodExpectingGenericIterator($mock_iterator);
$resulting_data = $mock_iterator->getArrayCopy();
Run Code Online (Sandbox Code Playgroud)