Saltar al contenido principal

Mutation Testing

Código Completo
El resultado final del código desarrollado en este documento se puede encontrar en el monorepo de GitHub springboot-demo-projects, bajo el tag .

¿Qué es Mutation Testing?

Pitest lo define así:

Mutation testing es conceptualmente bastante simple. Se siembran automáticamente fallos (o mutaciones) en tu código, luego se ejecutan tus tests. Si tus tests fallan, la mutación es eliminada, si tus tests pasan, la mutación sobrevive. La calidad de tus tests se puede medir a partir del porcentaje de mutaciones eliminadas.

Mutation testing no se discute frecuentemente en círculos de Spring Boot. Algunas razones de su popularidad limitada son:

  • Preocupaciones de performance: El mutation testing es computacionalmente caro, especialmente para codebases grandes de Spring.
  • Percepción de complejidad vs. valor: Muchos equipos cuestionan si los insights adicionales justifican la complejidad de setup y los costos de runtime.
  • Impacto en CI/CD: El largo tiempo de ejecución puede interrumpir los ciclos de feedback rápido en pipelines de CI/CD.

Hay un interés lentamente creciente, particularmente entre equipos con prácticas de testing maduras, pero preparate para tiempos de build más largos.

Por qué Mutation Testing No Tiene Sentido en Proyectos Groovy

El tipado dinámico de Groovy, el dispatch de métodos en runtime, el overloading de operadores, y el uso intensivo de AST transformations (@Canonical, @Builder, @Slf4j, Spock internals, etc.) significan que PIT muta bytecode generado que a menudo no se parece al código fuente que escribiste. Como resultado, muchos mutantes son insignificantes, inalcanzables, o sobreviven por razones no relacionadas con la calidad del test.

El mutation testing es una gran opción para Java y Kotlin, pero en proyectos Groovy produce ruido en vez de insights.

PIT Mutation Testing

Archivos a Crear/Modificar
File Tree
├── build.gradle
└── src/
└── ...

Configuremos el Gradle plugin for PIT Mutation Testing

build.gradle
plugins {
// ...
id 'info.solidsoft.pitest' version '1.19.0-rc.3'
}
// ...
tasks.named('check') {
// ...
dependsOn 'pitest'
}
// ...
pitest {
def basePackage = "${project.group}.${project.name}".toString()

targetClasses = [
"${basePackage}.config.advice.*",
"${basePackage}.config.log.*",
"${basePackage}.sakila.*.adapter.*",
"${basePackage}.sakila.*.domain.port.*",
] as Iterable<? extends String>

targetTests = ["${basePackage}.*"] as Iterable<? extends String>

excludedClasses = [
"${basePackage}.generated.*",
'**.*MapperImpl*',
] as Iterable<? extends String>

mutationThreshold = 70
coverageThreshold = 80

junit5PluginVersion = '1.2.3'
threads = 4
outputFormats = ['HTML']
timestampedReports = false
jvmArgs = [
'-XX:+EnableDynamicAgentLoading',
'--add-opens',
'java.base/java.lang=ALL-UNNAMED',
'--add-opens',
'java.base/java.util=ALL-UNNAMED',
'--add-opens',
'java.base/java.lang.reflect=ALL-UNNAMED',
'--add-opens',
'java.base/java.io=ALL-UNNAMED'
]
}

Ejecuta la tarea pitest. Cuando termine vas a tener un reporte HTML en build/reports/pitest/index.html.

Project Summary

Number of ClassesLine CoverageMutation CoverageTest Strength
6
95%
87/91
84%
21/25
87%
21/24

Breakdown by Package

NameNumber of ClassesLine CoverageMutation CoverageTest Strength
dev.pollito.spring_java.config.advice1
94%
18/19
100%
4/4
100%
4/4
dev.pollito.spring_java.config.log3
94%
51/54
78%
15/19
83%
15/18
dev.pollito.spring_java.sakila.film.adapter.in.rest1
100%
8/8
100%
1/1
100%
1/1
dev.pollito.spring_java.sakila.film.domain.port.in1
100%
10/10
100%
1/1
100%
1/1

Qué Significa Cada Métrica

MétricaQué MidePor Qué ImportaBuen Target
Line Coverage% de líneas de código ejecutadas durante testsFácil de lograr pero engañoso - números altos no significan buenos tests80%+ (estándar de la industria)
Mutation Coverage% de mutaciones matadas de todas las creadasLa cosa real - muestra cuántos bugs tus tests realmente atraparían~70%+ (indica tests sólidos)
Test StrengthMutaciones Matadas / Mutaciones CubiertasEfectividad de los tests sobre el código que realmente tocan80%+ (aserciones significativas)

Enfocate en mutation coverage y test strength por sobre el line coverage. Un 70% de mutation coverage es infinitamente más valioso que un 95% de line coverage con aserciones débiles. Si el test strength es bajo pero el line coverage es alto, tus tests están ejecutando código sin realmente verificar comportamiento.