Extreme mutation testing es una técnica relativamente nueva que nos ayuda a revisar si tenemos buenos casos de prueba de forma computacionalmente más eficiente que el mutation testing clásico.
Como sabemos, tener una buena batería de tests automáticos es fundamental en cualquier desarrollo, ya que ayudan a detectar de forma rápida y eficiente cualquier fallo que se haya introducido en nuestro código.
Normalmente se mira la cobertura por línea, que nos dice si una línea se ha ejercitado, pero no nos dice si estamos usando buenos casos de prueba, sólo si la línea se ejecuta. Podemos usar mutation testing para ver si estamos usando buenos casos de prueba.
Sobre mutation testing se ha hablado en mutation testing con pit y se recomienda su lectura. En este artículo nos centraremos en pit-descartes que es el motor que usa pit para hacer extreme mutation.
Este tipo de mutation testing fue propuesto en el paper Will My Tests Tell Me If I Break This Code?. Las ventajas que ofrece es que muta no a nivel de operadores, sino a nivel de método, lo que nos hace tener muchos menos mutantes dándonos una mayor rapidez y un buen punto de partida para saber donde tenemos que poner el foco en mejorar.
Índice de contenidos
1. Introducción
Mutation testing es una herramienta muy útil para ver cómo de buena es nuestra base de tests. Si bien, la mutación guiada por operadores puede ser lenta, es más precisa que la mutación extrema. La mutación extrema nos puede dar una primera visión del estado de nuestros tests, y luego pasar a una mutación clásica.
Es muy importante tener una buena cobertura de test con buenos casos de prueba (no solo aquellos que hacen que el código se ejecute) para que cuando se hagan modificaciones en el código, se detecten el mayor número de errores posibles. Con estas herramientas nos aseguramos que el ejercicio del código está realizado en base a unos valores y aserciones correctas.
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil Lenovo t480 (1.80 GHz intel i7-8550U, 32GB DDR4)
- Sistema Operativo: Pop OS 20.04 5.11.0-7620-generic #21~1626191760~20.04~55de9c3-Ubuntu.
- Entorno de desarrollo: IntelliJ IDEA 2021.2 (Ultimate Edition).
- Apache Maven 3.8.1.
- JDK AdoptOpenJDK-11.0.11+9
3. Extreme mutation testing
En los últimos años, ha surgido una variante del mutation testing que se hace llamar extreme mutation testing.
Esta variante surge de la necesidad de aumentar el rendimiento del mutation testing, siendo computacionalmente menos exigente.
Se basa en la idea de que en vez de mutar operadores o instrucciones, lo que hace es mutar métodos.
Si un método es mutado y a pesar de tener cobertura el test sigue pasando, es que no se está probando de forma adecuada, lo que se conoce como que los métodos están «pseudo testeados».
Como siempre vamos a tener menos métodos que operadores o instrucciones, se van a generar menos mutantes lo que hace que sea computacionalmente más rápido que el mutation testing tradicional.
4. Mutaciones
Cuando se mutan los métodos, los mutantes particulares tienen que sobrevivir para que un método se considere pseudo testeado.
A los métodos que no devuelven nada (void), se les vacía el cuerpo y si ese mutante resultante sobrevive, el método se clasifica como pseudo testeado. Si a un método se le quita el cuerpo y el test sigue pasando, es que hay algo raro ¿verdad?.
Para métodos que devuelven valores, lo más normal es que varias mutaciones tengan que sobrevivir.
Por ejemplo, si un método devuelve un booleano, dos mutantes tienen que sobrevivir. Para este caso se reemplaza el cuerpo del método con dos mutaciones, una que devuelve true y otra que devuelve false.
De las mutaciones anteriores miramos la supervivencia de los mutantes: si ambos mutantes sobreviven, el método se clasifica como pseudo testeado, pero si solo uno sobrevive, el método se clasifica como parcialmente probado.
Boolean sería el caso más fácil por solo tener dos valores, otros tipos como enteros (0, 1, 2, 3 … Etc.), cadenas («», «Cadena», «otra cadena») se eligen valores para que si los mutantes sobreviven, el método pueda ser categorizado como pseudo testeado.
Los objetos pueden ponerse a null.
La elección de los valores que se utilizan para la mutación depende de la herramienta de mutación. pitest-descartes es configurable y tiene estos valores por defecto.
5. PIT
En este artículo solo se va a hacer uso del plugin de maven y el código será el mismo que en el otro artículo pero con Junit5.
5.1. El proyecto de ejemplo
Tenemos una clase calculadora:
package com.examples;
public class Calculator {
public int addition(int firstNumber, int secondNumber) {
return firstNumber + secondNumber;
}
public int subtraction(int firstNumber, int secondNumber) {
return firstNumber - secondNumber;
}
public int multiplication(int firstNumber, int secondNumber) {
return firstNumber * secondNumber;
}
public int division(int firstNumber, int secondNumber) {
return firstNumber / secondNumber;
}
}
Y sus correspondientes tests:
package com.examples;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class CalculatorTest {
private final Calculator sut = new Calculator();
@Test
public void addition_should_add_two_numbers() {
assertThat(4, is(this.sut.addition(2, 2)));
}
@Test
public void subtraction_should_subtract_second_number_from_first_number() {
assertThat(6, is(this.sut.subtraction(8, 2)));
}
@Test
public void multiplication_should_multiply_two_numbers() {
assertThat(1, is(this.sut.multiplication(1, 1)));
}
@Test
public void division_should_divide_first_number_with_the_second() {
assertThat(5, is(this.sut.division(40, 8)));
}
}
Los tests están con Junit5 y hamcrest, por lo que necesitamos sus dependencias:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Antes de hacer nada, comprobamos que los tests pasan.
5.2. Uso de PIT a través de Maven
Para usar pit con maven, necesitamos añadir el plugin al pom.xml, tal y como se ve a continuación:
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.6.8</version>
<configuration>
<mutationEngine>descartes</mutationEngine>
</configuration>
<executions>
<execution>
<id>run-pitest</id>
<phase>test</phase>
<goals>
<goal>mutationCoverage</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>0.14</version>
</dependency>
<dependency>
<groupId>eu.stamp-project</groupId>
<artifactId>descartes</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
</plugin>
Es importante ver que dentro de dependencias tenemos dos: una es el plugin para poder usarlo con junit 5 (pitest-junit5-plugin) y la otra es el motor para hacer extreme mutation testing (descartes). También observamos que la configuración indica que estamos usando el motor descartes:
<mutationEngine>descartes</mutationEngine>
Otra cosa que necesita PIT es un engine para junit, por lo que necesitamos añadir la siguiente dependencia:
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.7.2</version> <scope>test</scope> </dependency>
Si no añadimos esa dependencia, tendremos un error similar a este:
3:44:15 PM PIT >> INFO : Verbose logging is disabled. If you encounter a problem, please enable it before reporting an issue.
3:44:15 PM PIT >> INFO : Incremental analysis reduced number of mutations by 0
3:44:15 PM PIT >> INFO : Created 1 mutation test units in pre scan
3:44:16 PM PIT >> INFO : Sending 2 test classes to minion
3:44:16 PM PIT >> INFO : Sent tests to minion
3:44:16 PM PIT >> SEVERE : Coverage generator Minion exited abnormally due to UNKNOWN_ERROR
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.973 s
[INFO] Finished at: 2021-08-07T15:44:16+02:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.pitest:pitest-maven:1.6.8:mutationCoverage (default-cli) on project extremeMutation: Execution default-cli of goal org.pitest:pitest-maven:1.6.8:mutationCoverage failed: Coverage generation minion exited abnormally!
Si ejecutamos:
mvn test && mvn org.pitest:pitest-maven:mutationCoverage
Podemos ir a target/pit-reports/[FECHA] y vemos el index.html con la información.
Desde la consola también se nos da información:
================================================================================
- Timings
================================================================================
> pre-scan for mutations : < 1 second
> scan classpath : < 1 second
> coverage and dependency analysis : < 1 second
> build mutation tests : < 1 second
> run mutation analysis : 1 seconds
--------------------------------------------------------------------------------
> Total : 1 seconds
--------------------------------------------------------------------------------
================================================================================
- Statistics
================================================================================
>> Line Coverage: 5/5 (100%)
>> Generated 8 mutations Killed 7 (88%)
>> Mutations with no coverage 0. Test strength 88%
>> Ran 11 tests (1.38 tests per mutation)
Lo que podemos observar es que aunque tenemos cobertura en todas las líneas, nos está fallando una mutación y nuestros tests podrían ser mejores.
Si entramos en el detalle:
Lo que nos está diciendo es que en el caso de la multiplicación, si cambia lo que devuelve el método por 1 y ese mutante sobrevive, lo que nos indica que ese test no está probando bien.
En efecto es así, porque hemos elegido multiplicar 1 * 1, el cual es un caso de prueba malo porque no nos permite detectar errores (1 / 1 = 1, o simplemente return firstNumber darían como bueno ese test).
Si cambiamos el test para que sea:
@Test
public void multiplication_should_multiply_two_numbers() {
assertThat(32, is(this.sut.multiplication(8, 4)));
}
Ahora tenemos un mejor caso de prueba.
Si volvemos a ejecutar:
mvn test && mvn org.pitest:pitest-maven:mutationCoverage
Vemos que tenemos una cobertura del 100% en líneas y también en mutación.
6. Conclusiones
Con el extreme mutation testing podemos con un menor coste computacional hacernos una idea de cómo de buenos son nuestros tests.
Este menor coste computacional es debido a que se hacen menos mutaciones, pero puede ser un buen punto de partida para ver en que estado está un proyecto e ir mejorando, y luego tener un grano más fino con los tests de mutación clásicos.