¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
dev
Ismail Ahmedov Hace 7 horas Cargando comentarios…
Quis custodiet ipsos custodes? Esta es una locución latina del poeta romano Juvenal, que significa ¿Quién vigilará a los vigilantes? Es una buena pregunta. ¿De verdad la ley es igual para todo el mundo? ¿Los vigilantes aplican la ley de manera correcta?
En el desarrollo del software, los vigilantes son los test. Sí, los tests deben asegurarse de que nuestro código cumple con los requisitos de negocio, es decir, que nuestro código hace lo que debe hacer. Los tests también son los guardianes que deben vigilar que no “rompamos” nada cuando hacemos alguna modificación, deben avisarnos.
Pero, ¿cómo podemos asegurarnos de que tenemos suficientes tests? Sí, podemos medir la cobertura de código, conocido más por su nombre en inglés Code Coverage:
En ingeniería de software, la cobertura de código, también llamada cobertura de tests, es una medida porcentual que indica el grado en que el código fuente de un programa se ejecuta al ejecutar un conjunto de pruebas determinado.
Como se puede ver de la definición, la cobertura de código solo mide el porcentaje de las líneas que se han ejecutado, no la calidad de los tests. He visto muchos proyectos con una cobertura de código casi de 100%, pero la mayoría de los tests eran irrelevantes o poco útiles. La cantidad y la calidad son diferentes cosas. Pero ¿cómo podemos detectar la calidad de nuestros tests? ¿Y quién controla si nuestros tests (los vigilantes) hacen bien su cometido? Los tests de mutación (Mutation Testing) nos ayudan con esta tarea.
La idea de mutation testing es modificar el código cubierto por tests de forma sencilla, comprobando si el conjunto de tests existente para este código detectará y rechazará las modificaciones. Se utiliza para diseñar nuevos tests y evaluar la calidad de los tests existentes. ¿Cuáles son los tests con mala calidad?
Mutation testing se basa en dos ideas:
La primera es la hipótesis del programador/a competente: hacen su trabajo de la mejor manera posible, pero podemos cometer pequeños errores que no están en la estructura del programa.
La segunda es el efecto de acoplamiento: los pequeños errores tienden a propagarse y generar fallos más complejos en el código, por lo que al detectar estos pequeños errores con pruebas de mutación, también se pueden descubrir defectos más graves.
Operadores de mutación/mutadores
Un mutador es la operación que se aplica al código original. Los ejemplos básicos incluyen el cambio de un operador '>' por un '<', la sustitución de los operadores '&&' por '||', y la sustitución de otros operadores matemáticos.
Mutantes
Un mutante es el resultado de aplicar el mutador a una entidad. Un mutante es una modificación del código en el tiempo de ejecución que se utilizará durante la ejecución del conjunto de pruebas.
Mutaciones matadas/supervivientes
Cuando se ejecuta el conjunto de los tests contra el código mutado, hay dos resultados posibles para cada mutante: el mutante ha muerto o ha sobrevivido. Un mutante eliminado significa que al menos una prueba ha fallado como resultado de la mutación. Un mutante que ha sobrevivido significa que nuestro conjunto de pruebas no ha detectado la mutación y, por tanto, debe mejorarse.
Como el mutation testing valida la calidad de nuestros tests, antes de lanzar las pruebas de mutación se ejecutan los tests y deben pasar. Sino, no se puede seguir.
Si todos los tests pasan correctamente, la librería de mutation testing empieza a crear mutaciones. Para cada mutación se ejecutan todos los tests.
Vamos a ver un ejemplo de 10 tests a los cuales se pueden crear 5 mutantes.
👍 Si nuestros tests fallan después de la mutación, entonces podemos decir que la mutación fue detectada y eliminada.
La calidad de los tests se mide en función del porcentaje de mutación eliminada. Las pruebas de mutación prueban si los tests son efectivos.
Este es el listado de las herramientas automatizadas para mutation Testing:
Vamos a verlo con un ejemplo de una kata llamada Fizz Buzz. Los requisitos de la kata son simples:
Aquí tenemos nuestro código de FizzBuzz.java:
public class FizzBuzz {
public String convert(int number) {
if (isDivisibleBy(3, number)) {
return "Fizz";
}
if (isDivisibleBy(5, number)) {
return "Buzz";
}
if (isDivisibleBy(15, number)) {
return "Fizz";
}
return String.valueOf(number);
}
private boolean isDivisibleBy(int divisor, int number) {
return number % divisor == 0;
}
}
Y este es nuestro FizzBuzzTest.java:
class FizzBuzzTest {
private FizzBuzz fizzBuzz;
@BeforeEach
void setUp() {
this.fizzBuzz = new FizzBuzz();
}
@ParameterizedTest
@CsvSource({"1,1", "2,2", "4,4"})
void convert_regular_number_to_string(int input, String expected) {
String actual = fizzBuzz.convert(input);
assertThat(actual).isEqualTo(expected);
}
@ParameterizedTest
@ValueSource(ints = {3, 6, 9})
void convert_numbers_divisible_by_3_and_not_divisible_by_5_to_Fizz(int input) {
String actual = fizzBuzz.convert(input);
assertThat(actual).isEqualTo("Fizz");
}
@ParameterizedTest
@ValueSource(ints = {5, 10, 20})
void convert_numbers_divisible_by_5_and_not_divisible_by_3_to_Buzz(int input) {
String actual = fizzBuzz.convert(input);
assertThat(actual).isEqualTo("Buzz");
}
@ParameterizedTest
@ValueSource(ints = {15, 30, 45})
void convert_numbers_divisible_by_15_to_Fizz(int input) {
String actual = fizzBuzz.convert(input);
assertThat(actual).isEqualTo("Fizz");
}
}
Revisamos el código y vemos que los tests pasan. El código no es perfecto pero nos parece correcto para lo que necesitamos ahora.
Los requisitos no funcionales son:
Vamos a añadir el plugin de Pitest a nuestro pom.xml y configurar el umbral de mutación a 95%.
<build>
<plugins>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.16.2</version>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.1</version>
</dependency>
</dependencies>
<configuration>
<mutationThreshold>95</mutationThreshold>
</configuration>
</plugin>
</plugins>
</build>
Yo estoy utilizando Maven en este ejemplo, pero si quieres usarlo con Gradle, puedes seguir este tutorial: Gradle quick start.
Probamos si funciona ejecutando los siguientes comandos:
mvn clean test
mvn pitest:mutationCoverage
Es necesario que se ejecuten los dos comandos en orden, porque Pitest trabaja con el código compilado, así que si no se compila el código y los tests, no veremos el resultado de los últimos cambios que hemos hecho. Si no hemos hecho ningún cambio, es suficiente ejecutar el segundo comando. Un comando más rápido puede ser:
mvn clean compile test-compile
mvn pitest:mutationCoverage
Pero a mí, personalmente, me gusta el feedback rápido, así que prefiero ejecutar primero los tests y después mutationCoverage. A continuación vemos que nos sale el siguente error:
La puntuación de mutación es de 90% y debe ser de al menos un 95%. Vamos a revisar los resultados del reporte de Pitest. Abrimos el index.html que se puede encontrar en la carpeta target -> pit-reports.
Lo abrimos en nuestro navegador favorito y revisamos los resultados:
Abrimos el informe hasta llegar a FizzBuzz.java y revisamos el reporte:
Vemos que en la línea 15 hay un mutante que ha sobrevivido. Abajo podemos ver las mutaciones:
Vemos que la sexta mutación consiste en cambiar el valor de retorno con un string vacío “” y el mutante no ha sido detectado por los tests.
Si revisamos el código y nos fijamos en la línea 15, vemos que la condición nunca se va a alcanzar porque, si el número es divisible por 15, es también divisible por 3, así que se cumpliría en la condición de la línea 7, cuya condición es que sea divisible por 3 y devuelva “Fizz”.
Movemos la comprobación si el números es divisible por 15 como primera instrucción del nuestro método convert(int number):
public class FizzBuzz {
public String convert(int number) {
if (isDivisibleBy(15, number)) {
return "Fizz";
}
if (isDivisibleBy(3, number)) {
return "Fizz";
}
if (isDivisibleBy(5, number)) {
return "Buzz";
}
return String.valueOf(number);
}
private boolean isDivisibleBy(int divisor, int number) {
return number % divisor == 0;
}
}
Ejecutamos de nuevo los tests de mutación, pasan correctamente y el build termina. Revisamos el reporte y vemos que la puntuación de mutación es de 100%.
Revisando los cambios detectamos que, con las prisas, habíamos cometido un error. Para los valores divisibles por tres y cinco devolvemos Fizz en vez de FizzBuzz. Vamos a arreglarlo en el código y en los tests y vemos que todo funciona correctamente.
Gracias a los tests de mutación podemos detectar pequeños errores que están hechos sin querer.
El análisis de mutantes supervivientes es fundamental para mejorar la calidad del código. Mientras que algunos revelan problemas significativos en el código o en los test, otros representan mutaciones equivalentes o simplemente ruido. Sin embargo, todos proporcionan información valiosa sobre la efectividad de las pruebas.
Podemos dividir los mutantes supervivientes en tres categorías:
En esta categoría podemos poner los:
Este código, en mayoría de los casos, es generado automáticamente con el IDE o usando Lombok, así que no aporta mucho valor si le hacemos pruebas de mutación. Se debe excluir de la cobertura de mutación.
Este grupo de mutantes nos da información valiosa para hacer un refactor. Estos mutantes nos pueden mostrar:
Son mutantes que revelan datos reales y/o problemas significativos en nuestro código o tests. Debemos hacerles caso y solucionar los problemas que nos muestran.
Existen infinitos cambios posibles dependiendo del tamaño de nuestro código. Estos son algunos los mutadores:
Si tenemos un método que no devuelve nada, significa que es un método con “side effect”, es decir, que cambia un estado global o algo en la infraestructura. Por esto, Pitest elimina todo el código del método para ver si los tests fallan:
Para que se pueda hacer mutation testing, nuestros tests deben cumplir los siguientes requisitos:
Los unit tests cumplen estos requisitos, así que debemos excluir las siguientes pruebas del mutation testing:
Las pruebas de mutación son una técnica avanzada de validación que ayuda a garantizar la calidad del código al evaluar la efectividad de los tests existentes. Su implementación aporta varios beneficios clave:
En resumen, la prueba de mutación es una técnica valiosa que eleva la calidad del software al fortalecer su sistema de pruebas, fomentando un código más eficiente y asegurando que los errores sean detectados antes de llegar a producción. Podéis ver todo el código con los commits por pasos en el repositorio GitHub fizzbuzz-mutation-testing.
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Cuéntanos qué te parece.