Как я могу издеваться над классом, который реализует интерфейс Iterator с помощью PHPUnit?

Как я могу высмеять зависимость для моего класса, который реализует Iterator интерфейс?

Ответы

Ответ 1

Есть несколько существующих решений этой проблемы онлайн уже, но все те, которые я видел, имеют сходную слабость: они полагаются на ->expects($this->at(n)). Функция "ожидает на" в PHPUnit имеет слегка странное поведение, поскольку счетчик предназначен для каждого вызова метода для макета. Это означает, что если у вас есть вызовы методов для вашего итератора за пределами прямой вперед 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;
}

Ответ 2

Если вам просто нужно протестировать общий ролик, то PHP (в расширении SPL, который не может быть отключен в PHP > 5.3), имеет встроенные оболочки массивов, которые реализуют Iterable: Итераторы SPL. например.

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

Ответ 3

Вот решение, которое сочетает в себе лучшее из обоих миров, используя ArrayIterator внутри:

Использование phpunit/phpunit средства

private function createSomeIteratorDouble(array $items = []): SomeIterator
{
    $someIterator = $this->createMock(SomeIterator::class);

    $iterator = new \ArrayIterator($items);

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

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

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

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

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

    return $someIterator;
}

Использование phpspec/prophecy

private function createSomeIteratorDouble(array $items = []): SomeIterator
{
    $someIterator = $this->prophesize(\ArrayIterator::class);

    $iterator = new \ArrayIterator($items);

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

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

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

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

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

    return $someIterator->reveal();
}