Python: Cómo hacer pruebas 4: pyDoubles
Tras las peticiones populares , no puedo evitar escribir este post antes de lo que esperaba.
Es la continuación del artículo Python: Cómo hacer pruebas(3), que parece haber tenido bastante éxito.
En este caso vamos a ver los dobles de prueba, y utilizaremos, cómo no, pyDoubles , creado principalmente por Carlos Blé y disponible en Debian gracias a mi buen amigo David Villa.
Actualización 2011-11-16: Corrijo el significado de las siglas F.I.R.S.T. (¡Gracias, David!).
Qué vamos a hacer
En las ocasiones anteriores me he centrado en pruebas unitarias de objetos que no tienen relación con el mundo exterior. Esto está muy bien, pero es una mala programación. En una programación más pythónica utilizaremos objetos. Montones de objetos. Y los objetos hablan entre ellos.
Una batería de pruebas unitarias que se precie tiene que cumplir las condiciones FIRST, a saber:
- Fast, deben ejecutarse en unos pocos segundos.
- Independant, evitando el scripting: una prueba es independiente de todas las anteriores y, por tanto, pueden ejecutarse en cualquier orden.
- Repeatable, de manera que podemos ejecutarla dos o más veces seguidas sin que afecte al resultado.
- Self-Validating, dando un valor booleano por respuesta, y evitando así buscar esta respuesta en un mar de logs.
- Timely, en el momento adecuado; de nada sirve escribir los tests después del código, ya que éste puede resultar difícil de probar.
Podéis consultar la definición original de estas reglas en el libro “Clean Code”, de Robert C. Martin, al final del capítulo 9, “Unit Tests”, páginas 130-131.
Supongamos por un momento que tenemos un objeto que depende de otro. ¿Cómo hacemos que sean las pruebas del primero sean independientes? Lo tenemos complicado. Bueno, pues aquí es donde entran los dobles de pruebas: Creamos un objeto que parece el original, pero que no es el original (bueno, o sí… ya lo veremos).
Puedo aseguraros que me ha resultado difícil encontrar un ejemplo en el que relacionar dos objetos, con un dominio conocido por todos, pequeño y sencillo, de manera que resulte altamente didáctico… Y resulta que tenía el caso justo enfrente de mí :D. Vamos a implementar un pequeño sistema de ficheros muy muy básico. Vamos a centrarnos en un directorio. Y nos vamos a centrar en dos operaciones: borrar y cambiar permisos. Claro… Tenemos el problema de que un directorio depende fuertemente de otros directorios y de archivos, pero no quiero meterme en la implementación de los archivos… ¿Cómo lo hacemos?
Archivos
Como he dicho, no queremos meternos en la implementación de archivos, así que esto es lo que vamos a necesitar (lo copio aquí para no estar repitiéndolo todo el rato):
|
|
Como véis, si llegáramos a entrar en alguna de las funciones, nuestros tests fallarían estrepitosamente. Tenemos que ser capaces de probarlo sin usar el archivo real.
Las Pruebas…
Para probarlo voy a usar nose , ¡¡que para eso expliqué en el artículo anterior cómo usarlo!!.
El problema de nose es que la clase de pruebas debe estar arriba del todo. Tenedlo en cuenta a la hora de ir componiendo los tests…
Directorios
Asumamos que nuestro código es legacy y ya teníamos las clases escritas… Así me ahorro bastante tiempo XD. Ahí va la clase Directorio:
|
|
Probando, que es gerundio
Añadiendo
Pues ya tenemos nuestros Directorios, (no) tenemos nuestros Archivos… ¡¡Vamos a probarlo!! Voy a necesitar una prueba de la que no os he hablado, y es que para probar que borramos, necesitamos añadir. ¿Estará funcionando correctamente este método? (nuevamente recuerdo que los tests deben ir al principio del archivo para que nose funcione):
|
|
¿Qué ha ocurrido aquí? ¿Qué es todo eso?
Bueno, es más sencillo de lo que parece. Lo que hacemos es usar pyDoubles, y por eso importamos todo lo que hay en el framework. Además, creamos una clase que hereda de TestCase para que se ejecuten las pruebas, y será en esta clase donde meteremos todos los tests.
El primero de los tests crea un archivo que no es un archivo… Es un “stub”. ¿Qué es eso?
Un STUB es un objeto que se comporta como el original, pero no es el original. Puede reemplazar un único método o un conjunto de ellos.
Como véis, lo que sea “archivo” no importa, ya que sólo me importa comprobar que, después, puedo recuperarlo. Técnicamente, y aprovechando la potencia de Python podría haber puesto cualquier cosa, pero usar un objeto pseudo-real es más claro. Si no me creéis, podéis comprobar que es un objeto de tipo Archivo.
Pero veamos el segundo ejemplo, que es más claro.
Borrando
En este caso vamos a borrar. Evidentemente, se tratará de un borrado recursivo, ya que si no pierde la gracia.
|
|
Aquí no he usado un stub, sino un spy, un espía.
Los SPY o spies son dobles de prueba como los stub, solo que tienen post-condiciones. Es decir: se crean, se usan y después se comprueba que se usaron correctamente.
Precisamente por eso está la comprobación final, en la que compruebo que el método “borrar” del objeto “archivo” se llamó. ¿Qué hizo? Pues nada, porque era un objeto de coña :D
Una de las grandes ventajas de pyDoubles es lo semántico que resulta su uso. ¡¡Casi se puede leer!!
Cambiando permisos
Ahora vamos con la segunda parte: comprobar que el cambio de permisos es recursivo. Es igual que el borrado, con la diferencia de que tenemos que pasar un parámetro:
|
|
Como podemos observar, en este caso hemos vuelto a utilizar un espía. Aquí comprobamos que el método se llamó con los permisos indicados, y también que el método borrar no se llamó nunca. Esto no es ni necesario, pero lo he puesto por mostrar más capacidades de pyDoubles.
Lo mismo se podría hacer de otra manera ligeramente diferente:
|
|
Aquí he utilizado un mock. Un mock es un stub con precondiciones. Es decir: primero digo lo que va a pasar, ejecuto, y compruebo que ha pasado.
Tan solo nos quedaría ver los “proxyspy”, que son espías que llaman al objeto real.
Todo junto
Aquí tenéis todo el código de un tirón:
|
|
Respuestas predefinidas
Habrá casos en los que necesitemos una respuesta determinada cuando se llame a un método, de manera que el algoritmo que estamos probando pueda continuar. Para ello podemos “prediseñar” métodos; ahí van diferentes maneras diferentes de hacerlo:
|
|
Cuestión de nomenclatura
La diferencia entre un stub, mock y spy es puramente teórica. Hay muchos frameworks que los confunden o ignoran. Yo, personalmente, utilizo siempre spies, ya que me parecen más intuitivos, aunque suelo llamarlos mocks, término con el que se suelen denominar globalmente a todos los dobles de prueba.
Hay más dobles de prueba, como los Fake, que son implementaciones completas de una interfaz pero con funcionalidad simplificada (imaginad una clase de acceso a base de datos que siempre devuelve datos fijos, sin acceder realmente). Si lo pensáis, un stub no es más que un fake automático XD. ¡¡Todos están relacionados!!
Así que, resumiendo:
- Es un stub si no se realizan comprobaciones.
- Será un mock si tenemos precondiciones.
- Un spy si tenemos postcondiciones.
- Un fake si usamos un objeto que implementa la funcionalidad simplificada.
Diré una cosa de corazón: llamadlos como queráis, pero no los mezcléis. La mezcla da lugar a métodos más complejos. Podéis usar distintos tipos en distintas pruebas, pero en una misma prueba, no los mezcléis. Y mucho menos con un único objeto.
Hamcrest y pyDoubles
Resulta que escribí el artículo anterior sabiendo lo que me hacía, y que
podemos combinar los matchers de Hamcrest junto a pyDoubles… ¿Cómo?
Pues, por ejemplo, en el método with_args
:
|
|
Como véis, hemos usado instance_of
, any_of
, starts_with
y ends_with
, que son matchers de Hamcrest.
Un gran poder conlleva una gran responsabilidad
… como le dijo el Tío Ben a Peter Parker :D
Tenemos en nuestras manos una gran herramienta, pero debemos tener en cuenta que debemos usarlo con cuidado, de la misma manera que un mecánico no arregla todas las averías con el mismo martillo.
Hay ciertas reglas básicas que cumplir, con el fin de evitar terminar con pruebas terriblemente complejas o nos metamos en camisas de once varas.
- Nunca “mockeéis” un objeto que no sea vuestro. De esta manera se evitan mocks interminables de todo Hibernate o cosas incluso peores. Es un fallo habitual en todo novato, y yo no fui la excepción.
- Realizad mocks precisos. Cuantas menos cosas reemplacéis, más estable será el test (en la terminología típica, “los tests serán menos frágiles”). Si estáis reemplazando muchos métodos, a lo mejor necesitáis un fake.
Para más información
Podéis visitar la web de pyDoubles , la de Hamcrest , la de nose , … O releer mis antiguos posts:
- Python: Cómo hacer pruebas(1)
- Python: Cómo hacer pruebas(2)
- Python: Cómo hacer pruebas(3)
- No comentes: ¡Asegura!
- Atheist: No seas crédulo ¡¡Prueba!!
No sé si me he dejado algo en el tintero. Tengo la sensación de que sí… sobre todo en las reglas a cumplir cuando usamos mocks. Toda contribución será bienvenida.
Un saludo!!