En este tutorial vamos a ver una técnica que nos permite comprobar la calidad de nuestras pruebas unitarias: mutation testing.
Ya sabemos que las pruebas unitarias, y los test en general, son una parte esencial en el ciclo de vida del software, nos ayudan a comprobar que el código fuente que tenemos entre manos es correcto, además de ofrecernos otras ventajas como por ejemplo documentación, simplificación de la integración, fomentan el cambio, etc.
Por desgracia, no siempre se entiende bien el concepto y la idea se pervierte, este es el caso de aquellos que introducen test sin una lógica consistente, sin verificaciones (verify) o sin aseveraciones (asserts), buscando el 100% de cobertura del código.Aquí es donde aparece el concepto de Mutation testing, que nos ayuda ha diseñar nuevas pruebas y a evaluar la calidad de las pruebas ya existentes.
Índice de contenidos
1. Introducción
El concepto Mutation testing fue acuñado por primera vez en 1978, un artículo de Richard J. Lipton[1], donde detallaba como, en un grupo formado por integrantes de la Universidad de Yale y del Instituto Tecnológico de Georgia, habían construido un sistema mediante el cuál determinar el grado en el que un determinado conjunto de pruebas había probado un programa Fortran. Al método que utilizaban en el sistema para realizar estas mediciones lo denominaba «Programa mutado».
A lo largo de este tutorial veremos en que consiste el Mutation testing, cuales son las mutaciones más relevantes y un ejemplo de aplicación con el plugin PIT para IntelliJ IDEA y Maven.
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro Retina 15′ (2.5 Ghz Intel Core I7, 16GB DDR3).
- Sistema Operativo: Mac OS Yosemite 10.10.5.
- Entorno de desarrollo: IntelliJ IDEA 14 CE.
- Apache Maven 3.3.0.
- JDK 1.7.0_79
3. Mutation testing
Básicamente, el Mutation testing consiste en introducir pequeñas modificaciones en el código fuente de la aplicación, a las que denominaremos mutaciones o mutantes.
- Si las pruebas pasan al ejecutarse sobre el mutante, el mutante sobrevive.
- Si las pruebas no pasan al ejecutarse sobre el mutante, el mutante muere
El objetivo es que todos los mutantes mueran, así podremos decir que el test responde a la definición concreta del código y que lo prueba correctamente.
Este concepto se basa en dos hipótesis:
- Hipótesis del programador competente: La mayoría de los errores introducidos por programadores Senior consisten en pequeños errores sintácticos.
- Hipótesis del efecto de acoplamiento: Pequeños fallos acoplados pueden dar lugar a otros problemas mayores.
Problemas de mayor orden serán revelados por mutantes de mayor orden, que se crean mediante la unión de multiples mutaciones.
4. Mutaciones
A continuación listamos algunas de las principales mutaciones:
- Reemplazo de valores lógicos: Cambiar true por false, y viceversa.
- Reemplazo de operadores aritméticos: Cambiar + por -, o * por /.
- Reemplazo de condiciones lógicas: Cambiar > por ≥ o por <.
- Eliminar líneas de código.
- Modificar contadores.
Este es solo un pequeño listado de todas las posibles, un listado m´s extenso puede visualizarse en el siguiente enlace: PIT mutation operators.
5. PIT
5.1. Instalación en IntelliJ IDEA 14 CE
Para proceder con la instalación del plugin acceder a Preferencias > Plugins > Browse Repositories… , en la ventana emergente buscamos «PIT», lo seleccionamos entre las opciones que se encuentran y pulsamos sobre el botón «Install plugin».
Cuando haya finalizado el proceso de instalación, reiniciar el IDE.
5.2. El proyecto de ejemplo
Para ver el funcionamiento del plugin, vamos a implementar una calculadora muy básica, que implementará 4 operaciones: Suma, resta, multiplicación y división.
Generamos un proyecto maven:
El código de nuestra clase será el siguiente:
Calculadora.java
package com.ejemplos; public class Calculadora { public int suma(int operando1, int operando2){ return operando1 + operando2; } public int resta(int operando1, int operando2){ return operando1 - operando2; } public int multiplica(int operando1, int operando2){ return operando1 / operando2; } public int divide(int operando1, int operando2){ return operando1 / operando2; } }
Como véis, hemos introducido un bug en la implementación del método suma, introduciendo la multiplicación, para ver como reaccionan las mutaciones al test.
El código de nuestra clase de test quedará de la siguiente manera:
Calculadora.java
package com.ejemplos; import org.hamcrest.MatcherAssert; import org.junit.Before; import org.junit.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; public class CalculadoraTest { private Calculadora sut; @Before public void inicializa(){ this.sut = new Calculadora(); } @Test public void suma_debe_devolver_la_suma_de_los_operandos(){ assertThat(4,is(this.sut.suma(2, 2))); } @Test public void resta_debe_devolver_la_resta_de_los_operandos(){ assertThat(6,is(this.sut.resta(8, 2))); } @Test public void multiplica_debe_devolver_la_multiplicacion_de_los_operandos(){ assertThat(1,is(this.sut.multiplica(1, 1))); } @Test public void divide_debe_devolver_la_division_de_los_operandos(){ assertThat(5,is(this.sut.divide(40, 8))); } }
Al ejecutar nuestro conjunto de test, obtenemos que todo ha sido correcto aunque, como hemos visto, nuestro código es erróneo y nuestro código de test lo secunda….
5.3. Uso de PIT a través de IntelliJ
Para poder utilizar el plugin PIT, basta con que lo activemos mediante Run > Edit Configurations…, pulsamos sobre el símbolo ‘+’ y añadimos un PIT runner, que mantendrá una configuración por defecto a la que debemos añadir la dirección en la que se generan las clases con el parámetro adicional «–targetClasses».
A continuación, procederemos a ejecutar el conjunto de test con el PIT runner que hemos definido:
Esta ejecución nos generará un reporte, que al abrirlo nos dará el análisis sobre las mutaciones que se han realizado…
Como vemos, una de las mutaciones ha sobrevivido, lo que implica que nuestro test no es consistente, y es posible que nuestro código no este haciendo lo que esperamos que haga.
5.3. Uso de PIT a través de Maven
Existe también la posibilidad de hacer uso de esta herramienta través de su plugin de maven, para ello tan solo hace falta agregar las siguientes líneas en el fichero pom.xml, en la sección de plugins.
<plugin> <groupId>org.pitest</groupId> <artifactId>pitest-maven</artifactId> <version>LATEST</version> <configuration> <mutators> <mutator>ALL</mutator> </mutators> <threads>10</threads> <targetClasses> <param>com.ejemplos*</param> </targetClasses> </configuration> </plugin>
Para ejecutarlo, tendremos que dirigirnos a un terminal e introducir el comando:
mvn org.pitest:pitest-maven:mutationCoverage
La ejecución del anterior comando ofrecerá los mismos resultados, un informe sobre el análisis de las mutaciones realizadas.
Éste tipo de ejecución permite una configuración mayor de los parámetros como las mutaciones que se van a aplicar, el código sobre el que se va a ejecutar o que clases excluir.
Para más información al respecto de la configuración podéis visitar el siguiente enlace: PIT maven configuration.
6. Conclusiones
En el plugin de PIT podemos encontrar un buen aliado a la hora de auditar código de testing, ya sea propio o ajeno.
A través de las diferentes mutaciones, podremos analizar si nuestras pruebas son consistentes y prueban nuestro código «como es debido».
Sin embargo, hay que tener en cuenta el conjunto de pruebas sobre el que aplicamos el plugin, así como el código que prueba, puesto que la complejidad y la cantidad del código hace que los tiempos de ejecución de las mutaciones y sus aplicaciones puede dispararse.
7. Referencias
- [1]RA DeMillo, RJ Lipton, FG Sayward – Computer, 1978 – computer.org.
- [2]Softwarecraftmanship Barcelona, 2015, Vicenç García-Altés, Mutation testing.
- [3]PIT Mutation Testing
- [4]Mutation testing, Wikipedia
- [5]Kill the mutants, Roy van Rijn, 2014, SlideShare