En esta charla vamos a hablar principalmente de testing aplicado a PHP. Daremos un pequeño y rápido repaso a PHPunit, si algún asistente no lo conoce, y después nos meteremos de lleno en el meollo de la cuestión que son los "test doubles": veremos qué son, para qué sirven y como trabajar con ellos cómodamente en PHP gracias a la librería Mockery. Para terminar la sesión veremos algunos ejemplos prácticos en los que veremos aplicados algunos de los conceptos teóricos vistos anteriormente.
4. ¿De qué va esto?
❖ Unit testing
❖ PHPUnit
❖ Test Doubles
❖ Mockery
5. DISCLAIMER
❖ Esta es mi manera de hacer las cosas,
seguro que las hay mejores.
❖ No soy un experto en testing, pero a
veces hago tests ;).
❖ Esto no trata de TDD, aunque es posible
que el término aparezca en más de una
ocasión.
6. Unit Test
Herramienta que nos permite comprobar el correcto funcionamiento
de una unidad de código. Esto sirve para asegurar que cada una
de las partes del sistema funciona correctamente por separado.
Debe ser:
❖ Atómico
❖ Independiente
❖ Inocuo
❖ Rápido
7. Atómico
❖ El test prueba la mínima cantidad de funcionalidad
posible .
❖ Un solo comportamiento del método o función.
❖ Distintos caminos de ejecución => un test para
cada camino
8. Independiente
❖ El resultado de la ejecución de un test no debe
depender de otros.
❖ No debe ser parte de una secuencia de tests que
deba ejecutarse en un orden determinado
9. Inocuo
❖ La ejecución de un test no debe alterar el estado del
sistema (ficheros, base de datos, etc) => no
mancha :)
❖ El resultado debe ser el mismo ya lo ejecutemos una
o mil veces => idempotente
10. Rápido
❖ Normalmente lanzaremos un gran número de tests
en cada ejecución
❖ Tener que esperar resulta improductivo y aburrido
=> no haríamos test (menos de los que ya hacemos
:P)
❖ Un test unitario debería ejecutarse en una pequeña
fracción de segundo
11. Características ATRIP
❖ Automatic: pueden ser corridos mediante un simple comando.
❖ Thorough (minucioso): cuanto más testemos mejor.
❖ Repeatable: podemos ejecutarlo una y mil veces con el
mismo resultado.
❖ Independant: el resultado de un test no debe depender de
otros.
❖ Professional: debemos ser igual de exigentes al escribir los
tests de lo que lo somos al escribir nuestra lógica de negocio
(prohibidas ñapas en los tests)
13. Un poco de terminología
❖ SUT: Subject/System Under Test.
❖ Colaboradores: objetos que interactúan con el
SUT y que necesita para implementar su
funcionalidad.
❖ Fixtures: Algo así como los elementos de contexto,
datos u objetos que necesitamos para construir el
escenario que requiere el test.
14. Anatomía de un unit test:
AAA
❖ Arrange: configuración y/o carga de fixtures,
instanciar colaboradores, etc.
❖ Act: ejercitar la funcionalidad que queremos
testear.
❖ Assert: afirmar que el estado es el esperado
15. PHPUnit
❖ Versión para php de los frameworks xUnit
❖ Creado por Sebastian Bergman
❖ Estándar de facto en php
❖ https://phpunit.de/
17. TestCase
❖ Los tests se organiza en clases que heredan de la clase
PHPUnit_Framework_TestCase
❖ Cada unit test es un método de la clase
❖ Para que un método sea considerado un test, su nombre debe llevar el
sufijo test o debe estar marcado con la anotación @test
18. setUp() y tearDown()
❖ setUp(): método que se ejecuta antes de cada test,
pude servirnos para la fase de arrangement.
❖ tearDown(): método que se ejecuta después de
cada test, puede utilizarse para labores de limpieza
21. Organización de los tests
❖ Fichero de configuración:
❖ Sistema de archivos:
• phpunit.xml.dist
• phpunit.xml
22.
23. Mocks
❖ Son objetos que imitan a otros objetos de nuestro sistema y
que utilizamos en su lugar a la hora de hacer tests => “Test
Doubles”
❖ Suelen reemplazar a los colaboradores en los tests unitarios
❖ En ocasiones pueden incluso reemplazar al SUT!!
❖ Permiten aislar la funcionalidad del SUT de la de los
colaboradores
❖ Facilitan la configuración del escenario del test
❖ Permiten que los tests unitarios sean rápidos
25. ¿Todos los Test Doubles
son Mocks?
❖ No
❖ Mock es una palabra empleada en el lenguaje
informal para referirse a los test doubles en
general.
❖ Existen varios tipos específicos de test doubles.
❖ Los mocks son un tipo específico de test double
❖ Vamos a verlos!
26. Dummies
❖ Es un test doble que sólo se usa para rellenar
parámetros de un constructor o método
❖ Nunca es usado dentro del la funcionalidad del SUT
que vamos a ejercitar (al menos en el camino de
ejecución que vamos a testear)
❖ En teoría sus métodos deben devolver NULL (si lo
generamos a mano implementando una interface)
❖ Generalmente implementarán una interface
27. Dummies
Algunos ejemplos robados de http://php-and-symfony.matthiasnoback.nl/2014/07/test-doubles/
❖ Dummies con phpunit:
❖ Ejemplo con mockery:
28. Stubs
❖ Son test doubles que devuelve un valor fijo
predeterminado en las llamadas a sus métodos
❖ No tiene en cuenta los argumentos pasados a los
métodos (no se verifican)
❖ No tiene en cuenta el número de llamadas
realizadas a los métodos
30. Fakes
❖ Son una especie de stubs con cierta lógica distinta de la
verdadera lógica del colaborador al que imita y normalmente
más ligera
❖ Suelen emplearse cuando el SUT depende de un colaborador
que no está disponible, es lento o simplemente dificulta el test
❖ Tampoco tiene en cuenta el número de llamadas a los métodos
❖ No realiza verificación de parámetros
❖ Pueden volverse tan complicados que requieran sus propios
tests unitarios
32. Spies
❖ Es una especie de fake o stub que que recaba
información en las llamadas realizadas a sus
métodos y permite inspeccionarla después
❖ Puede por ejemplo registrar qué métodos han sido
llamados, número de veces, parámetros recibidos,
etc.
34. Mocks
❖ Son un tipo muy especial de test double
❖ Se parece a los spies en el sentido de que registran
las llamadas realizadas y los parámetros pasados
❖ Es mucho más potente que un spy
❖ Permite algo que el resto no: validación del
comportamiento o interacción
35. Mocks
❖Permite definir un conjunto de expectativas para validar:
• Llamadas a los métodos
• Parámetros pasados: número, tipo y valor
• Número de veces llamado: exacto, mínimo, máximo
• Orden de las llamadas
❖Pueden actuar como stubs devolviendo valores
❖Existen un gran número de librerías que permiten generar
mocks de objetos (o interfaces) al vuelo, en php: phpunit,
mockery, prophecy, fake, etc.
37. To mock or not to mock?
❖ Los mocks son una herramienta muy potente y su uso tiene sus pros y sus
contras
❖ Su uso puede llevar a sobrespecificación y dar lugar a tests frágiles
❖ El uso o no de los mismos permite diferenciar dos tipos de validación:
• Validación del estado: propiedades de objetos, valores devueltos, etc.
• Validación del comportamiento o interacción: llamadas a métodos,
número de veces, orden, argumentos, etc.
❖ Esto a su vez da lugar a dos estilos de TDD:
• Classicist TDD (Chicago School): basado en la validación del estado
• Mockist TDD (London School): basado en la validación del
comportamiento
38. Classic TDD vs Mockist TDD
❖ TDD con mocks permite seguir un diseño outside-in:
• Se empieza creando un test para la capa más externa del sistema y se
mockean lo colaboradores
• Posteriormente se desarrollan tests para los colaboradores
• Así sucesivamente se van descubriendo los objetos del sistema y su api
❖ TDD clásico suele seguir un diseño middle-out:
• Se elige una característica del dominio por la que comenzar la
implementación
• Se avanza desarrollando el resto de objetos del dominio necesarios para
el funcionamiento de dicha característica
• Una vez desarrollada se construye la capa superior
39. Classic TDD vs Mockist TDD
❖ El uso de mocks suele hace que la fase de preparación del tests sea
más costosa ya que hay que definir los mocks y sus expectativas
❖ En el TDD clásico se suele reutilizar el código de configuración
entre varios tests, este suele ir el método setUp
❖ Un fallo en un objeto de un sistema con mocks causará fallos sólo en
los tests del objeto
❖ Un fallo en un objeto de un sistema clásico puede provocar fallos en
los tests de todos los objetos que tienen al objeto como colaborador
=> puede ser complicado localizar el fallo
❖ Hacer tests de granularidad fina minimizará este problema
❖ Como ventaja los tests clásicos comprueban la integración real de
los objetos del sistema
40. Classic TDD vs Mockist TDD
❖ Los tests con mocks están muy acoplados a la
implementación del SUT => cambios en la
implementación provocarán ruptura de los tests
❖ Los tests clásicos sólo se preocupan del estado,
da igual cual sea la implementación si el
resultado es el esperado
❖ La refactorización del código testado con mocks
conllevará un trabajo importante de refactorización
de los tests
41.
42. Mockery
❖ Librería php que nos permite crear mocks al vuelo
❖ https://github.com/padraic/mockery
❖ http://docs.mockery.io/en/latest/
❖ Instalación:
$ composer require mockery/mockery
43. Integración con phpunit
public function tearDown() {
Mockery::close();
}
<listeners>
<listener class="MockeryAdapterPhpunitTestListener"></listener>
</listeners>
❖ En el fichero de configuración:
❖ En cada test case:
❖ Si no hacemos esto no se validarán las expectativas
44. Mockeando
$mock = Mockery::mock(‘foo');
// Mock llamado “foo”
$mock = Mockery::mock(array('foo'=>1,'bar'=>2));
// Mock sin nombre con array de espectativas
$mock = Mockery::mock('foo', array('foo'=>1,'bar'=>2));
// Mock “foo” con espectativas
$mock = Mockery::mock('stdClass');
// Mockeando clases
$mock = Mockery::mock(‘PSRLogLoggerInterface’);
// Mockeando interfaces
$mock = Mockery::mock('stdClass, MyInterface1, MyInterface2');
// Mockeando una clase e implementando interfaces
45. Definiendo espectativas
• shouldReceive(method_name)
// El método debe ser llamado
• shouldReceive(method1, method2, ...)
//Varios métodos
• shouldReceive(array('method1'=>1, 'method2'=>2, …))
// Los métodos deben ser llamados y devolverán estos valores
• with(arg1, arg2, ...) / withArgs(array(arg1, arg2, ...))
// validando los parámetros recibidos
• withAnyArgs(), withNoArgs() //…
• andReturn(value) / andReturn(value1, value2, ...)
// estableciendo valores devueltos en la llamada / llamadas
• andReturnNull() / andReturn([NULL]) / andReturnValues(array) /
andReturnUsing(closure, ...)
46. Definiendo espectativas
• andThrow(Exception) / andThrow(exception_name, message)
// lanzando excepciones en la llamada
• andSet(name, value1) / set(name, value1)
// establecer valor de atributos en la llamada
• passthru() // llama a la verdadera implementación del método
• zeroOrMoreTimes() / once() / twice() / times(n) / never() /
atLeast() / atMost() / between(min, max)
// estableciendo el número de llamadas: exacto, mínimo, máximo, entre
• ordered() / ordered($group)
// define el orden de llamadas o el orden dentro de un grupo
• globally() // el orden se entre todos los mocks, no sólo el actual
• byDefault() // define expectativas por defecto, que puden ser
sobreescritas en los tests concretos
47. Validando argumentos
• with(‘some-value’) // con un valor concreto
• with(Mockery::any()) OR with(anything()) // cualquier valor
• with(Mockery::type('resource')) OR with(resourceValue()) OR
with(typeOf(‘resource')) // con un tipo concreto
• with(Mockery::on(closure)) // validando mediante un closure
• with('/^foo/') OR with(matchesPattern(‘/^foo/')) // regexp
• with(Mockery::ducktype('foo', ‘bar')) //cualquier objeto que
contenga los métodos indicados
• with(Mockery::mustBe(2)) OR with(identicalTo(2)) // ===
• with(Mockery::not(2)) OR with(not(2)) // negando
• with(Mockery::anyOf(1, 2)) OR with(anyOf(1,2)) // varias opciones
48. Validando argumentos
• with(Mockery::notAnyOf(1, 2)); // que no esté entre las opciones
• with(Mockery::subset(array(0 => ‘foo’)));
// un array que contenga este subconjunto
• with(Mockery::contains(value1, value2));
// un array que contenga estos valores
• with(Mockery::hasKey(key));
// un array con una clave
• with(Mockery::hasValue(value));
// con un valor
49. Partial Mocks
• $mock = Mockery::mock(‘MyClass[foo, bar]’);
Método tradicional, mockea los métodos foo y bar de la clase,
hay que definir su comportamiento mediante expectativas.
• $mock = Mockery::mock(‘MyClass')->makePartial();
Método pasivo, las llamadas son enviadas a los métodos originales,
excepto si se han definido expectativas para el método.
• $mock = Mockery::mock(new MyClass);
Mock parcial con proxy, crea un proxy que intercepta las llamadas en
lugar de heredar del objeto a mockear, esto permite mockear clases
declaradas “final” o con métodos “final”. El inconveniente es que no
validará los type hints ya que no extiende la clase a mockear.
❖ Permiten mockear sólo algunos métodos de los objetos
50. Y más en la documentación…
❖ Mockear atributos públicos
❖ Métodos estáticos
❖ Conservar el paso de parámetros por refencia
❖ Mockear cadenas de Demeter
❖ Grabación de interacciones con objetos reales
❖ …