Introducción
No siempre las pruebas unitarias que realizamos dan la cobertura suficiente a nuestro código, no nos engañemos. Allá por los años 70-80 se creo un nuevo concepto de testing denominado mutation testing. Consistía en modificar ciertas lineas de nuestro código, para posteriormente probar si en realidad fallaba o no.
Estos cambios en el código se denominan mutantes, y como si de un juego se tratará, debemos matarlos, es decir, si los teses fallan, se dice que han matado a los mutantes. Cuantos más mutantes matemos, mejor.
En este post vamos a ver como se realiza mutation testing mediante PIT, una herramienta que se va a encargar de realizar las mutaciones y ejecutar nuestros teses unitarios para ver si logra matar todos los mutantes.
Para ello, he dejado en mi github el proyecto con el que he realizado las pruebas. Podéis descargarlo y jugar a matar mutantes 😉
El código es “refactorizable”, lo sé, pero lo que he intentado es disponer de código sencillo que nos permita jugar con las pruebas unitarias y las mutaciones, lo vamos a ver.
Mutación del código
Podéis ver todos los tipos de mutaciones que realiza PIT en el siguiente enlace, pero vamos a ver una mutación sencilla, para ayudarnos a entender mejor lo que son las mutaciones en código.
La mutación de limites condicionales, consiste en cambiar los operadores relacionales de nuestras sentencias. Así comprobaremos que nuestras condiciones están bien construidas. Lo vemos en la siguiente tabla:
Original conditional | Mutated conditional |
---|---|
< | <= |
<= | < |
> | >= |
>= | > |
En nuestro código tenemos el siguiente condicional:
De forma que la mutación condicional que se hará será:
Cuando ejecutemos las pruebas de mutación, PIT se encargará de realizar todas las mutaciones. Por defecto se aplican las mutaciones básicas, pero si queremos ser más especificos o llegar un mayor nivel de mutación, PIT nos ofrece una lista de mutaciones que tendremos que activar por configuración.
Lanzando las pruebas de mutación
A pesar de que las pruebas de mutación nos ayudan a detectar errores en nuestro código, también son muy útiles para comprobar si nuestras pruebas unitarias son correctas o garantizan la cobertura necesaría, por lo que son un gran apoyo.
Veamos la lógica de nuestro código y las pruebas unitarias que garantizan que el código funcione correctamente. Para el producto que queremos comprar, se comprueba que haya la cantidad disponible y en caso correcto se decrementa en 1 el valor de la cantidad y se devuelve. Sencillo.
Nuestras pruebas unitarias comprobaran que se descuentan correctamente los productos y que no podemos comprar más productos de los que dispone la máquina.
Si ejecutamos la pruebas unitarias, nos darán un resultado verde, es decir, todo funcional correctamente! (Según nuestras pruebas)
Ahora es el turno de lanzar nuestras pruebas de mutación. Para ello, nos situamos en la raiz del proyecto y ejecutamos:
➜ mutation-testing git:(master) ./gradlew pitest
Las pruebas generan un reporte que se almacena en {project}/build/reports/pitest/. Vamos a analizarlos.
En nuestro proyecto de pruebas tenemos dos clases en src/java. En este caso la clase ProductCode es un enum, por lo que vamos a necesitar cobertura de test.
La lógica se encuentra en la clase VendingMachine.java, que es la encargada de gestionar la tienda.
En nuestras pruebas unitarias pensabamos que podríamos cubrir todos los casos, pero PIT nos muestra que no es así. Vamos a ver la razón. Las lineas verdes indican que PIT ha matado a los mutantes, mientras que las lineas rojas nos indican que los mutantes han sobrevivido.
Al fijarnos en la lista de mutaciones, vemos que en las líneas 20 y 27 se han realizado dos mutaciones de límite condicional que no han pasado las pruebas.
Si nos fijamos en el código de nuestras pruebas unitarías, vemos que en el test buy_correct_quantity_of_products, no comprobamos en todos los casos que ocurriría sí:
this.pXQuantity = quantity
Vamos a añadir los casos al test a ver que nos dice PIT:
Con los cambios realizados, hemos conseguido matar a todos los mutantes, por lo que los teses han pasado.
Conclusiones
Las pruebas unitarías tienen una gran importancía en nuestros desarrollos, si las apoyamos con pruebas de mutación conseguimos garantizar una mayor cobertura.
Con las pruebas de mutación no solo nos curamos en salud de que nuestro código funcione correctamente, si no que comprobamos que nuestras pruebas den la cobertura que esperamos.
Se escucha poco hablar sobre el “mutation testing”, pero bajo mi opinión es un buena práctica el meterlo en nuestra estrategía de calidad, ya que es un arma más para mejorar el alcance de nuestras pruebas.