¿Cómo puedo burlarme de una clase que implementa la interfaz Iterator usando PHPUnit?

5 minutos de lectura

Avatar de usuario de Dan
Dan

¿Cómo puedo simular una dependencia para mi clase que implementa el Iterator interfaz de una manera robusta?

Avatar de usuario de Dan
Dan

Ya hay un par de soluciones existentes para este problema en línea, pero todas las que he visto comparten una debilidad similar: se basan en ->expects($this->at(n)). La función ‘espera en’ en PHPUnit tiene un comportamiento ligeramente extraño en el sentido de que el contador es para cada llamada de método al simulacro. Esto significa que si tiene llamadas de método a su iterador fuera de un foreach directo, debe ajustar su simulacro de iterador.

La solución a esto es crear un objeto que contenga los datos básicos del iterador (matriz de origen y posición) y pasar eso a returnCallback cierres Debido a que se pasa por referencia, el objeto se mantiene actualizado entre llamadas, por lo que podemos simular cada método para simular un iterador simple. Ahora podemos usar el simulacro de iterador de forma normal sin tener que preocuparnos por un orden de llamada rígido.

Método de muestra a continuación que puede usar para configurar un simulacro de iterador:

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

  • Estoy publicando una solución a un problema que encontré. Como no tengo blog lo pongo aquí. Supongo que es una forma válida de usar SO, ya que hay una casilla de verificación para hacerlo cuando publica una pregunta 🙂

    – Dan

    09/04/2013 a las 17:33


  • ¡Buena solución reutilizable!

    – gusano

    22 de agosto de 2014 a las 8:30

  • ¡lindo! acabo de cambiar sizeof() a count()

    – boobiq

    26 de enero de 2015 a las 18:09

  • Volviendo a esta pregunta (porque he notado que sigo obteniendo puntos de ella) con un par de años más de experiencia en pruebas de unidades en mi haber, ahora le preguntaría si realmente necesita simular ese iterador personalizado. Vale la pena pensar cuánto beneficio obtendrá y cuánta complejidad agregará a su prueba al simular cosas. Me gustaría darme hace dos años el beneficio de la duda y asumir que tenía una buena razón para este simulacro, pero tengo la sensación de que realmente me hubiera ido mejor usando la clase real.

    – Dan

    23 de marzo de 2015 a las 13:51


Si solo necesita probar contra un iterador genérico, entonces PHP (en la extensión SPL, que no se puede desactivar en PHP> 5.3) tiene contenedores de matriz integrados que implementan Iterable: Iteradores SPL. p.ej

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

avatar de usuario de localheinz
localheinz

He aquí una solución que combina lo mejor de ambos mundos, utilizando un ArrayIterator internamente:

Con phpunit/phpunit burlándose de las instalaciones

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

Con 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();
}

  • ¿Por qué la gente se molesta con ->expects($this->any())es como ESTO DEBE PASAR, pero cero, una o muchas veces, que es el 100% del diagrama de posibilidades de Venn, ¿no?

    – código de jengibre Ninja

    18 de junio de 2019 a las 13:31

  • @gingerCodeNinja Ya no escribiría dobles de prueba como este, lo actualizaré más tarde cuando esté en una computadora.

    – localheinz

    18 de junio de 2019 a las 16:03


  • Sí, acabo de darme cuenta de que era una respuesta súper antigua, todo bien. (Sin embargo, todavía veo a personas haciéndolo, lo cual es extraño; necesito preguntarles por qué la próxima vez que lo atrape).

    – código de jengibre Ninja

    18 de junio de 2019 a las 23:57

  • @gingerCodeNinja ¡Ajusté mi respuesta!

    – localheinz

    19 de junio de 2019 a las 11:14

  • @gingerCodeNinja El ->expects($this->any()) no está allí solo para probar si se llama ‘cualquier’ número de veces. También está ahí para permitir que quien esté revisando las pruebas sepa explícitamente que una función en particular no tiene ninguna restricción en la cantidad de veces que se llamará. Es útil en TDD. En el caso de este simulacro de iterador, no es necesario ya que solo se usará como muestra.

    – Pantera negra

    11 de enero de 2022 a las 6:49

¿Ha sido útil esta solución?