Dan
¿Cómo puedo simular una dependencia para mi clase que implementa el Iterator
interfaz de una manera robusta?
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()
acount()
– 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();
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