Test de mutación

Introducción

Como desarrolladores de software siempre queremos entregar la máxima calidad que podamos dar. Creamos tests, medimos la cobertura y confiamos en que son fiables… hasta que aparece un bug en producción justo en una zona que se supone que está cubierta.

Esto nos lleva a plantearnos las siguientes preguntas:

¿Nuestros tests realmente prueban el código o solo lo recorren sin validar nada útil?

¿Cómo podemos medir la eficacia real de nuestras pruebas?

Para responder a esto, necesitamos algo más que cobertura de líneas. Necesitamos una técnica que evalúe la calidad de los tests en sí.

Ahí es donde entran los tests de mutación

A lo largo de este artículo, explicaremos en qué consisten los test de mutación, cómo se aplican, qué tipos existen, cuándo conviene usarlos y qué herramientas hay disponibles para implementarlos en distintos lenguajes de programación.

¿Qué son los tests de mutación?

Los tests de mutación son una serie de técnicas que consisten en introducir pequeñas modificaciones en el código fuente y comprobar si los tests unitarios detectan estos cambios, con el objetivo de evaluar la eficacia de nuestros tests. 

Si nuestros tests no detectan estos cambios, es una indicación de que puede que no se estén cubriendo de forma adecuada ciertos flujos.

¿En qué consiste el proceso de mutación?

El proceso se puede desglosar en los siguientes puntos:

Ejecución de los tests iniciales

Con esto comprobaremos si la batería de tests original funciona. Si falla, no podemos fiarnos de los que lancemos cuando el código mute.

Generación de mutantes

Se crean modificaciones del código fuente. Estas mutaciones las podemos categorizar en:

Mutación de valores

Consiste en realizar un cambio en valores constantes o fijos para detectar que se prueban los límites correctamente.

Tomemos como ejemplo un método que comprueba si la edad proporcionada es la de un adulto.

Mutación de valores

Los tests generados para este método podrían ser los siguientes:

Los tests generados para este método podrían ser los siguientes:

Si el valor constante 18 mutase a otro, como 21, el test que valida que la edad 18 es adulta, fallaría, indicando que el test ha reaccionado correctamente a la mutación del código.

Mutación lógica

Se basa en realizar cambios en los operadores lógicos con el objetivo de comprobar que todas las condiciones se cumplen.

Pongamos un ejemplo para entenderlo mejor:

Tenemos un método que comprueba si el importe de una compra tiene descuento o no.

Tenemos un método que comprueba si el importe de una compra tiene descuento o no.

Hemos creado estos tests para probarlo:

Hemos creado estos tests para probarlo:

Si la condición del método a comprobar es modificada, por ejemplo, de mayor a menor de 100, ésto haría que el primer test falle demostrando que la mutación ha sido detectada.

Mutación aritmética

Son aquellas mutaciones que afectan a los cálculos. Sólo los tests que validan con precisión los valores esperados y las fórmulas clave podrán detectarlos.

No basta con probar que “algo se calcula”; hay que comprobar cómo se calcula exactamente.

Veamos un ejemplo:

Vamos a probar un método que calcula el precio total al aplicar el impuesto.

 Vamos a probar un método que calcula el precio total al aplicar el impuesto.

Los tests asociados serían los siguientes:

Los tests asociados serían los siguientes:

Si en la mutación se cambia el símbolo más (+) por menos (-), se restaría la cantidad haciendo que el test fallase y comprobando así que es sensible a la mutación.

Mutación de enunciado

Esta mutación consiste en el reemplazo o eliminación de código fuente para comprobar que los tests detectan la falta de comportamiento esperado.

Usemos un ejemplo de un carrito de la compra para verlo más claro:

 Usemos un ejemplo de un carrito de la compra para verlo más claro:

El test que podemos generar para comprobar el método podría ser este:

El test que podemos generar para comprobar el método podría ser este:

Si analizamos este test, podemos ver que no se está comprobando el nuevo valor de stock y por lo tanto si una mutación eliminara la línea donde se reduce el stock, no lo detectaría.

Vamos a hacer un cambio para que compruebe que el stock se ha reducido:

Vamos a hacer un cambio para que compruebe que el stock se ha reducido:

Con esto, comprobamos tanto que se ha añadido una cantidad de artículo a la cesta como que el stock de este artículo ha disminuido.

Ejecución de tests contra cada mutante

Se ejecutan los tests unitarios creados pero en cada versión mutada del código.

Evaluación del resultado

Para cada mutante se evalúa si los tests lo detectaron o no:

  • Mutante asesinado: al menos un test falla, lo que indica que el test detectó el error introducido.

  • Mutante superviviente: todos los tests pasan, mostrando que los tests no detectaron la mutación y puede indicar posible falta de cobertura o aserción débil.

  • Mutante inválido: la mutación rompe la compilación o genera un error sintáctico, y por lo tanto se descarta.

  • Mutante equivalente: el código mutado hace lo mismo que el original, por eso las pruebas no pueden notar la diferencia. Estos mutantes son “imposibles” de eliminar.

Cálculo del Mutation Score

Se calcula un porcentaje que representa la efectividad de la batería de tests.

Mutation score =Mutantes asesinados / (Mutantes asesinados+supervivientes) X 100

Con esto sabremos qué calidad tienen nuestros tests, revelando si hay casos sin cubrir o si son débiles.

¿Qué podemos considerar un buen mutation score?

Aunque esto es subjetivo y variable, podemos dividirlo en 3 rangos:

  • Superior a 85%, podemos decir que nuestros tests son muy buenos porque detectan la mayoría de mutaciones.
  • Entre 60% y 85%, consideraremos que tenemos buenos tests pero que hay un margen de mejora amplio.
  • Menor a 60%, tendremos que considerar que debemos enfocarnos en posibles escenarios que no se estén contemplando y rehacer tests que sean débiles.

 

Hay que tener en cuenta que puede haber mutantes equivalentes que hagan que el mutation score no aumente aunque nos esforcemos mejorando los tests.

 

Aplicación del proceso de mutación

Para resumir lo visto anteriormente, vamos a crear un ejemplo paso a paso que resume los conceptos tratados, haciéndolo lo más simple posible para enfocarnos exclusivamente en el proceso.

Y con todo lo mencionado, empecemos:

Utilizaremos una clase llamada ItemService donde implementaremos 2 métodos públicos:

  • El primero se llamará calculateTotalPrice, que se encargará de calcular el precio de un artículo. Recibirá 2 parámetros: 
    • customerAge: la edad del cliente
    • quantity: cantidad
  • El segundo será getStock, que nos dará el stock actual que hay del artículo.

Además tendremos un método privado para saber si aplicar un descuento o no.

Además tendremos un método privado para saber si aplicar un descuento o no.

 

Una vez tenemos la clase creada, generamos los tests:

Una vez tenemos la clase creada, generamos los tests

Hecho esto, ejecutamos los test que hemos creado:

Hecho esto, ejecutamos los test que hemos creado:

Una vez que hemos comprobado que todos los test se ejecutan correctamente, vamos a generar las mutaciones y ejecutarlas. Para ello, nos vamos a servir de la herramienta PitTest, de la que hablaremos más adelante.

 Para ello, nos vamos a servir de la herramienta PitTest, de la que hablaremos más adelante.

Como vemos, nuestros test tienen un alto porcentaje de cobertura, 94%, pero no están detectando algunas de las mutaciones realizadas, hay 9 sin detectar.

Si nos fijamos en la imagen nuestro mutation score, en pit test llamado test strength, es de un 79%.

Analizando en detalle el informe, comprobamos que hay varias mutaciones supervivientes.

Analizando en detalle el informe, comprobamos que hay varias mutaciones supervivientes.

Ahora, vamos a matar las mutaciones supervivientes realizando modificaciones en nuestra clase de test:

Como primer test, vamos a crear uno donde se realice el cálculo de una compra de 100 unidades y que verifique que, tras la operación, el stock disponible es 0.

Ahora, vamos a matar las mutaciones supervivientes realizando modificaciones en nuestra clase de test: Como primer test, vamos a crear uno donde se realice el cálculo de una compra de 100 unidades y que verifique que, tras la operación, el stock disponible es 0.

Con esto, lo que queremos probar es que el valor del stock inicialmente es 100.

 Con esto, lo que queremos probar es que el valor del stock inicialmente es 100.

El fragmento donde se comprueba si hay suficiente stock para realizar el cálculo y el que resta el stock disponible.

El fragmento donde se comprueba si hay suficiente stock para realizar el cálculo

Con esto matamos las mutaciones que cambian el valor del stock de 100 a 101, las que modifican la condición y las que modifican la resta del stock.

y el que resta el stock disponible.

 

 

Los siguientes tests que vamos a crear, van dirigidos a matar las mutaciones que afectan a la condición que decide si se aplica el descuento o no.

Con esto matamos las mutaciones que cambian el valor del stock de 100 a 101, las que modifican la condición y las que modifican la resta del stock. Los siguientes tests que vamos a crear, van dirigidos a matar las mutaciones que afectan a la condición que decide si se aplica el descuento o no.

Estos comprobarán los límites de los valores y los criterios para asegurar que la condición se cumple.

Estos comprobarán los límites de los valores y los criterios para asegurar que la condición se cumple.

Ahora que tenemos todos estos nuevos tests creados, vamos a ejecutar la herramienta para ver si los hemos matado.

Ahora que tenemos todos estos nuevos tests creados, vamos a ejecutar la herramienta para ver si los hemos matado.

Con esta imagen del reporte, podemos comprobar que hemos matado todas las mutaciones que se generan.

¿Cuándo son recomendables y cuándo no?

Los tests de mutación son recomendables cuando se quiere comprobar la calidad de los tests unitarios realizados, especialmente en proyectos con un nivel alto de automatización. También nos permiten detectar si nuestros tests están dando falsos positivos o falsos negativos. 

Son muy útiles antes de desplegar una release crítica, como una que tenga una funcionalidad clave o que sea una major, para asegurarnos de que los tests realmente validan el comportamiento esperado.

Por el contrario, no son recomendables cuando no hay una base sólida de test, ni en proyectos pequeños o sin mantenimiento dado que el coste-beneficio es muy bajo. Tampoco cuando la cantidad de recursos (tiempo, equipo, infraestructura…) es limitada y deben priorizarse otras tareas.

Además, debemos considerar que el tiempo de ejecución se puede incrementar debido a que cada mutación requiere volver a ejecutar la batería de pruebas, impactándonos en los procesos de integración continua si no se mantiene dentro de unos límites razonables.

¿Qué diferencia hay entre los tests de mutación y de regresión?

Ahora que sabemos algo más sobre test de mutación, nos puede entrar la siguiente duda: 

¿Qué nos aportan los tests de mutación si con los tests de regresión ya estamos comprobando la calidad de la aplicación?

Aunque ambos se centran en analizar el software, sus objetivos son diferentes. 

Tipo de Test Propósito Objetivo
Test de regresión Asegurar que las funcionalidades existentes siguen funcionando tras cambios en el código. Evitar que se introduzcan errores involuntarios en partes del sistema que ya funcionaban.
Test de mutación Evaluar la capacidad de los tests existentes para detectar cambios en el comportamiento del código. Medir la calidad y efectividad de los tests, no la funcionalidad directa de la aplicación.

Ambos tipos de test son complementarios y nos ayudarán a que nuestro software sea más robusto y que tenga más calidad.

Herramientas para realizar mutaciones

Aunque los ejemplos mostrados anteriormente están creados en lenguaje Java, vamos a listar distintas herramientas de mutaciones para diferentes lenguajes.

Lenguaje de programación Herramienta de mutación Características principales
Java y JVM PiTest
  • Realiza mutaciones a nivel de bytecode.
  • Plugins para Maven, Gradle y Ant.
  • Integración con Jenkins y Sonarqube.
Javascript, Typescript, C#, .NET, Scala Stryker
  • Paraleliza las mutaciones para mejorar el rendimiento.
  • Tiene un modo incremental para reutilizar resultados de mutantes previos cuando el código o los tests no han cambiado.
  • Compatible con NPM y YARN.
Python MutPy
  • Compara la duración de ejecución de la mutación con el test original para prevenir pruebas que tardan demasiado 
  • Funciona con pytest, unittest y nose
  • Genera los reportes en YAML y HTML
C/C++ Mull
  • Solo recompila fragmentos modificados del código para mejorar el rendimiento
  • Se integra con herramientas como CMake, Ninja, Make, o Clan

Conclusión

Mientras que la cobertura de código nos dice qué se ejecuta, los tests de mutación nos revelan qué tan bien estamos probando nuestra aplicación.

A pesar de que su implementación requiere un esfuerzo adicional, dota a nuestros desarrollos de mayor calidad y fiabilidad permitiendo identificar debilidades y errores en etapas tempranas del ciclo de vida del software.

Si alguna vez te has preguntado si tus tests son suficientes, es hora de dejar de asumir y empezar a comprobarlo.

Anímate a probarlos y haz que tus aplicaciones alcancen su máximo potencial.

















<span style="font-size:80%">Autor </span><a href="https://blog.kairosds.com/author/andresgonzalez/" target="_self">Andrés González</a>

Autor Andrés González

Sep 16, 2025

Otros artículos