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

时间:2021-12-02 20:47:25

How can I mock a dependency for my class that implements the Iterator interface in a robust manner?

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

3 个解决方案

#1


15  

There's a couple of existing solutions to this problem online already but all of the ones I've seen share a similar weakness: they rely on ->expects($this->at(n)). The 'expects at' function in PHPUnit has slightly odd behaviour in that the counter is for every method call to the mock. This means that if you have method calls to your iterator outside of a straight forward foreach you have to adjust your iterator mock.

已经有几个现有的解决方案已经解决了这个问题,但我见过的所有解决方案都存在类似的缺点:它们依赖于 - >期望($ this-> at(n))。 PHPUnit中的'expect at'函数有一些奇怪的行为,因为计数器用于模拟的每个方法调用。这意味着如果您在直接foreach之外调用迭代器,则必须调整迭代器模拟。

The solution to this is to create an object holding the basic iterator data (source array and position) and pass that into returnCallback closures. Because it's passed by reference the object is kept up to date between calls so we can mock each method to simulate a simple iterator. Now we can use the iterator mock as normal without having to worry about a rigid call order.

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

Sample method below that you can use to setup an iterator mock:

下面的示例方法,您可以使用它来设置迭代器模拟:

/**
 * 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;
}

#2


5  

If you just need to test against a generic iterator, then PHP (in the SPL extension - which can't be turned off in PHP > 5.3) has built in array wrappers that implement Iterable: SPL Iterators. e.g.

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

$mock_iterator = new \ArrayIterator($items);
$test_class->methodExpectingGenericIterator($mock_iterator);
$resulting_data = $mock_iterator->getArrayCopy();

#3


2  

Here's a solution which combines the best of both worlds, using an ArrayIterator internally:

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

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

    $iterator = new \ArrayIterator($items);

    $someIterator
        ->expects($this->any())
        ->method('rewind')
        ->willReturnCallback(function () use ($iterator) {
            $iterator->rewind();
        })
    ;

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

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

        })
    ;

    $someIterator
        ->expects($this->any())
        ->method('next')
        ->willReturnCallback(function () use ($iterator) {
            $iterator->next();
        })
    ;

    $someIterator
        ->expects($this->any())
        ->method('valid')
        ->willReturnCallback(function () use ($iterator) {
            return $iterator->valid();
        })
    ;

    return $someIterator;
}

#1


15  

There's a couple of existing solutions to this problem online already but all of the ones I've seen share a similar weakness: they rely on ->expects($this->at(n)). The 'expects at' function in PHPUnit has slightly odd behaviour in that the counter is for every method call to the mock. This means that if you have method calls to your iterator outside of a straight forward foreach you have to adjust your iterator mock.

已经有几个现有的解决方案已经解决了这个问题,但我见过的所有解决方案都存在类似的缺点:它们依赖于 - >期望($ this-> at(n))。 PHPUnit中的'expect at'函数有一些奇怪的行为,因为计数器用于模拟的每个方法调用。这意味着如果您在直接foreach之外调用迭代器,则必须调整迭代器模拟。

The solution to this is to create an object holding the basic iterator data (source array and position) and pass that into returnCallback closures. Because it's passed by reference the object is kept up to date between calls so we can mock each method to simulate a simple iterator. Now we can use the iterator mock as normal without having to worry about a rigid call order.

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

Sample method below that you can use to setup an iterator mock:

下面的示例方法,您可以使用它来设置迭代器模拟:

/**
 * 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;
}

#2


5  

If you just need to test against a generic iterator, then PHP (in the SPL extension - which can't be turned off in PHP > 5.3) has built in array wrappers that implement Iterable: SPL Iterators. e.g.

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

$mock_iterator = new \ArrayIterator($items);
$test_class->methodExpectingGenericIterator($mock_iterator);
$resulting_data = $mock_iterator->getArrayCopy();

#3


2  

Here's a solution which combines the best of both worlds, using an ArrayIterator internally:

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

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

    $iterator = new \ArrayIterator($items);

    $someIterator
        ->expects($this->any())
        ->method('rewind')
        ->willReturnCallback(function () use ($iterator) {
            $iterator->rewind();
        })
    ;

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

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

        })
    ;

    $someIterator
        ->expects($this->any())
        ->method('next')
        ->willReturnCallback(function () use ($iterator) {
            $iterator->next();
        })
    ;

    $someIterator
        ->expects($this->any())
        ->method('valid')
        ->willReturnCallback(function () use ($iterator) {
            return $iterator->valid();
        })
    ;

    return $someIterator;
}