Mutation testing con Approvals en Java: un problema inesperado
Publicado por Fran Reyes & Manuel Rivero el 24/05/2026
Introducción.
En post anteriores[1] hemos hablado sobre approval testing, una herramienta que nos facilita algunos de los pasos más engorrosos de la técnica de Golden Master + Sampling, y sobre mutation testing, una técnica que puede darnos información sobre la capacidad de detección de errores de nuestra suite de tests.
Usar estas técnicas conjuntamente nos permite mejorar de manera iterativa nuestra golden master para generar unos tests más robustos. En esta ocasión vamos a explorar la combinación de ambas técnicas en Java.
Tests de golden master iniciales.
Partimos de unos test de golden master que generamos haciendo un sampling grabando el input y el output de ejecuciones reales del juego Ugly Trivia[2]. Para poder escribir los tests además tuvimos que romper varias dependencias incómodas usando la técnica Extract and Override Call[3] lo que nos permitió controlar desde los tests el input y el output indirecto que usa el juego[4].
Aplicando mutation testing con PIT vimos que nuestro golden master consigue un 86% de mutation coverage.
Los mutantes supervivientes estaban en su mayoría en las seams o en código superfluo, y por tanto, no eran mutantes relevantes.
Tests más simples y fáciles de mantener gracias a approval testing.
Como se puede observar en el test anterior, el output que usamos en la verificación del test es un array de strings de gran tamaño.
Si alguno de estos tests fallase cuando estamos añadiendo nuevo comportamiento, tendríamos que comparar el output del golden master con el nuevo output generado para comprobar si las diferencias se deben a un error o a un cambio de comportamiento intencionado. En caso de que la diferencia no sea causada por un error tendremos que actualizar el golden master manualmente. Esta comparación de outputs y actualización de la golden master puede ser un proceso bastante engorroso.
Es precisamente en esos pasos de comparación de outputs y actualización del golden master donde approval testing nos facilita mucho la vida[5].
Para usar approval testing en nuestros tests añadimos la librería ApprovalTests.Java a nuestro proyecto y utilizamos Approvals.verifyAll en la parte de la verificación del test. Así quedan los tests después de estos cambios:
A continuación, ejecutamos los test y aprobamos el output directamente, ya que
ni el código de producción ni el input utilizado en los tests han cambiado. Como resultado se genera un fichero llamado GameTest.simulation_with_three_players.approved.txt que contiene el output del golden master contra el que se verificará de aquí en adelante en nuestro test el output real que genere el código de producción.
Finalmente podemos eliminar el método expectedMessagesWithThreePlayers que devolvía el output del golden master que estábamos utilizando en la verificación en la versión anterior de los tests. Ese mismo output está ahora en GameTest.simulation_with_three_players.approved.txt.
Como vemos, nuestros tests son ahora más simples, nos hemos ahorrado tanto obtener el output del golden master como escribir la aserción que lo compara con el output real generado por el código de producción.
Además, tanto la comparación del output real generado por el código bajo tests y del output del golden master, como el proceso de aprobación (actualización) del output del golden master son gestionados a partir de ahora por la herramienta de ApprovalTests.Java. Esto reduce significativamente la fricción de la técnica de golden master, facilitando así el mantenimiento y evolución de estos tests.
Combinando mutation testing con approval: un problema inesperado.
Los cambios que introdujimos al introducir ApprovalTests.Java no modificaron ni el input, ni el output, ni el código de producción que cubre nuestro test, por tanto, el mutation coverage obtenido al aplicar mutation testing debería ser exactamente el mismo que obtuvimos con la versión inicial de los tests: 86%.
Sin embargo al ejecutar PIT con la nueva versión de los tests obtuvimos un resultado inesperado:
Sorprendentemente el mutation coverage aumentó a un 93% 😱!!.
¿Cómo es posible si el input y el output son los mismos y la herramienta ApprovalTests.Java no altera el código de producción?
Tuvimos que investigar qué estaba pasando.
En busca de los mutantes perdidos.
Al comparar el informe generado por PIT de la clase testeada en la versión usando ApprovalTests.Java. (fragmento a la izquierda) y la versión sin approval (fragmento a la derecha) observamos que algunos mutantes supervivientes en la versión inicial de los tests habían sido eliminados en la versión de los tests usando ApprovalTests.Java.
En la versión de los tests usando ApprovalTests.Java sólo quedaban mutantes supervivientes en las seams .
La IA no fue capaz de detectar por qué perdíamos mutantes supervivientes, y empezó a alucinar un montón de posibles soluciones que conducían a callejones sin salida. Para llegar al origen del problema tuvimos que investigarlo nosotros mismos de forma sistemática, estableciendo y comprobando diferentes hipótesis.
Primera hipótesis.
Nuestra primera hipótesis fue que, por algún motivo desconocido, algunas de las mutaciones generadas para la versión inicial se estaban dejando de generar en la versión con ApprovalTests.Java. Para verificar esta hipótesis ejecutamos PIT en modo verboso y analizamos si se seguían introduciendo mutaciones en esas líneas del código de producción. Esto es lo que observamos en el log de PIT:
Tras contrastar que, en efecto, sí que se producían mutaciones, por ejemplo, en las líneas 10 y 23 del informe, y que el número total de mutaciones era exactamente el mismo para ambas versiones de los tests, descartamos esta hipótesis.
Segunda hipótesis
La siguiente hipótesis que consideramos fue que los tests estuvieran fallando al ejecutar ApprovalTests.Java junto con PIT. No encontramos ningún indicio en los logs, lo que nos llevó a pensar que, de existir ese error, se lo podría estar tragando alguna de las librerías.
Para confirmar o descartar esta última hipótesis, envolvimos la aserción de ApprovalTests.Java con un try-catch mostrando por consola el error capturado.
Este fue el nuevo output que apareció en el log de PIT:
Así que, efectivamente, el test de ApprovalTests.Java estaba lanzando una excepción al ser ejecutados por PIT, que PIT se estaba “tragando”, y aún peor, interpretando como que los tests fallaban. Un test que falla significa que el test detecta el error introducido por la mutación, lo que hacía que PiT considerara que el mutante no sobrevivía, y de ahí el aumento inesperado en el mutation coverage.
Esto explica también que los únicos mutantes supervivientes estuviesen en las seams, ya que, para ahorrar tiempo, PIT no ejecuta los tests para mutantes generados en código no cubierto por los tests.
El motivo por el que ApprovalTests.Java lanza esta excepción parece estar relacionado con la ejecución en paralelo que realiza PIT y con el acceso concurrente a los ficheros que ApprovalTests.Java escribe en disco para comparar el output real generado con el código de producción con el output del golden master.
La solución.
Por suerte, en la misma descripción del error se incluían varias alternativas para solucionarlo. De las opciones disponibles, la que tenía más sentido para nuestro caso era la de configurar ApprovalTests.Java para permitir múltiples invocaciones en paralelo de verifyAll.
Para ello añadimos lo siguiente en el setup del test Approvals.settings().allowMultipleVerifyCallsForThisClass().
Con esto conseguimos evitar el error y volver a obtener, con la versión de los tests usando ApprovalTests.Java, el mismo mutation coverage del 86% que habíamos obtenido con la versión inicial de los tests.
Es muy importante tener este problema en cuenta para que ambas herramientas puedan funcionar bien de manera conjunta.
Conclusión: la importancia de entender bien las técnicas y herramientas que usamos.
La experiencia que contamos en este post nos recuerda que integrar herramientas no siempre es transparente. Aunque el comportamiento funcional de los tests no había cambiado, la interacción entre ApprovalTests.Java y PIT provocó resultados engañosos en la métrica de mutation coverage debido a problemas de concurrencia y manejo interno de excepciones.
Ser capaz de detectar este problema requiere entender bien tanto approval testing como mutation testing para saber qué invariante debía seguir cumpliéndose tras refactorizar los tests para introducir approval testing, (el mutation coverage, en este caso), y poder usarlo para validar si el refactoring había ido bien.
La IA no nos ayudó a detectar la causa del problema, sus alucinaciones nos condujeron a varios callejones sin salida. Al final, tuvimos que investigarlo nosotros mismos, teniendo que comprender el funcionamiento interno de las herramientas implicadas para poder formular hipótesis y contrastarlas sistemáticamente. Fue un ejemplo de cómo, la IA no siempre sustituye la necesidad de entender en profundidad las técnicas y herramientas que utilizamos.
Agradecimientos.
Nos gustaría agradecer a Fernando Aparicio, Emmanuel Valverde, Alfredo Casado y Antonio de la Torre por revisar el borrador de este post.
Referencias.
Notas.
[1] En nuestro blog puedes encontrar otros posts interesantes tanto sobre Mutation Testing como sobre Approval Testing.
[2] Existen diferentes estrategias de sampling para generar nuestro golden master (input y output). Las principales estrategias de sampling son:
a. Fingir el input y grabar el output correspondiente.
b. Generar input aleatorio y grabar el output correspondiente.
c. Grabar el input y el output de ejecuciones reales (preferentemente en producción).
[3] Michael Feathers describe la técnica de ruptura de dependencias Extract & Override Call en el capítulo 25 de su libro, Working Effectively with Legacy Code.
[4] Nosotros enseñamos técnicas para trabajar con código legacy como golden master + sampling, mutation testing, approval testing, extract & override call y muchas otras en nuestras formaciones Cambiando Legacy y Técnicas de Testing para desarrolladores.
[5] Para los que no conozcan nada de approval testing este es un pequeño fragmento del material que aparece en nuestras formaciones Cambiando Legacy y Técnicas de Testing para desarrolladores:
Approval testing facilita la aplicación de la técnica de Golden Master, aportando una herramienta que nos ayuda con algunos de los pasos más complicados y/o engorrosos de dicha técnica:
- la comparación entre el resultado real y el aprobado (golden master),
- la visualización de las diferencias entre ellos (si las hay), y
- el proceso de aprobación de un nuevo resultado válido (actualización del golden master).