| name | mutation-testing |
| description | Workflow para correr mutation testing en un codebase y analizar la calidad de la suite de tests. Usá este skill cuando el usuario quiera: medir qué tan buenos son sus tests, encontrar huecos en la cobertura de tests, correr mutation testing, analizar mutantes sobrevivientes, mejorar la efectividad de los tests, o saber si sus tests realmente detectan bugs. También triggerá cuando el usuario mencione "mutation testing", "mutation score", "mutantes sobrevivientes", "calidad de tests", "correr pitest", "correr stryker", "correr mutmut", "correr cargo-mutants", o pregunte si sus tests son buenos o suficientes.
|
Mutation Testing Skill
Workflow para medir la calidad de los tests usando mutation testing: introducir pequeños cambios en el código y verificar si los tests los detectan.
Concepto clave
Un mutante es una versión del código con un cambio pequeño (ej: > → >=, + → -, eliminar una condición).
Si los tests fallan con el mutante → el mutante fue eliminado (tests buenos ✅).
Si los tests pasan con el mutante → el mutante sobrevivió (hueco en los tests ⚠️).
El mutation score = mutantes eliminados / total mutantes. Un score >80% es generalmente aceptable; >90% es excelente.
Paso 0: Detectar ecosistema y herramienta
Identificar el lenguaje y la herramienta de mutation testing disponible:
ls package.json pom.xml build.gradle Cargo.toml pyproject.toml setup.py Gemfile go.mod 2>/dev/null
| Ecosistema | Herramienta recomendada | Cómo verificar si está configurada |
|---|
| JavaScript / TypeScript | Stryker | cat stryker.config.* o jq '.stryker' package.json |
| Java / Kotlin | PIT (Pitest) | grep -r 'pitest|pit-' pom.xml build.gradle |
| Python | mutmut | pip show mutmut o cat .mutmut-config |
| Ruby | mutant | bundle exec mutant --version |
| Rust | cargo-mutants | cargo mutants --version |
| Go | go-mutesting | go-mutesting --version |
Si no hay herramienta configurada, ir al Paso 1b para instalarla.
Paso 1a: Verificar configuración existente
Si la herramienta ya está instalada, revisar su configuración:
Stryker (JS/TS)
cat stryker.config.js stryker.config.mjs stryker.config.json 2>/dev/null
jq '.stryker // empty' package.json 2>/dev/null
Pitest (Java/Kotlin)
grep -A 30 'pitest' pom.xml
grep -A 20 'pitest\|info.solidsoft' build.gradle build.gradle.kts
mutmut (Python)
cat setup.cfg .mutmut-config 2>/dev/null | grep -A 20 '\[mutmut\]'
cargo-mutants (Rust)
cat .cargo/mutants.toml 2>/dev/null
Paso 1b: Instalar/configurar la herramienta (si no está)
Stryker (JS/TS)
npx stryker init
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
Configuración mínima en stryker.config.mjs:
export default {
testRunner: 'jest',
coverageAnalysis: 'perTest',
reporters: ['html', 'clear-text', 'progress'],
thresholds: { high: 80, low: 60, break: null },
}
Pitest (Maven)
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.16.1</version>
<configuration>
<targetClasses>
<param>com.miapp.*</param>
</targetClasses>
<targetTests>
<param>com.miapp.*Test</param>
</targetTests>
<outputFormats>
<outputFormat>HTML</outputFormat>
<outputFormat>XML</outputFormat>
</outputFormats>
</configuration>
</plugin>
Pitest (Gradle/Kotlin)
plugins {
id("info.solidsoft.pitest") version "1.15.0"
}
pitest {
targetClasses.set(setOf("com.miapp.*"))
threads.set(4)
outputFormats.set(setOf("XML", "HTML"))
mutationThreshold.set(80)
}
mutmut (Python)
pip install mutmut
cargo-mutants (Rust)
cargo install cargo-mutants
Paso 2: Correr mutation testing
⚠️ Mutation testing es lento por naturaleza (puede tardar de minutos a horas en proyectos grandes). Para una primera corrida, acotar el scope.
Stryker (JS/TS)
npx stryker run
npx stryker run --mutate "src/utils/**/*.ts"
Pitest (Maven)
mvn test-compile org.pitest:pitest-maven:mutationCoverage
mvn org.pitest:pitest-maven:mutationCoverage \
-DtargetClasses="com.miapp.MiServicio" \
-DtargetTests="com.miapp.MiServicioTest"
Pitest (Gradle)
./gradlew pitest
./gradlew pitest --tests "com.miapp.MiServicioTest"
mutmut (Python)
mutmut run
mutmut run --paths-to-mutate src/mi_modulo.py
cargo-mutants (Rust)
cargo mutants
cargo mutants --file src/lib.rs
Paso 3: Leer e interpretar los resultados
Stryker — resultados en consola y HTML
Pitest — XML de resultados
find target/pit-reports -name 'mutations.xml' | sort | tail -1 | xargs cat
mutmut — estado de mutantes
mutmut results
mutmut show <id>
mutmut html
cargo-mutants — resultados
cat mutants.out/outcomes.json | jq '.outcomes | group_by(.outcome) | map({outcome: .[0].outcome, count: length})'
Presentar al usuario un resumen con:
- Mutation score total
- Cantidad de mutantes: total / eliminados / sobrevividos / timeout
- Archivos con peor score (los que más necesitan mejora)
Paso 4: Analizar mutantes sobrevivientes
Los mutantes sobrevivientes son los más valiosos: indican comportamiento no testeado.
Clasificar por tipo de mutación
| Tipo de mutante | Qué significa si sobrevive |
|---|
Cambio de operador (> → >=) | Los tests no prueban el caso límite (boundary) |
| Eliminación de condición | Hay paths de código no ejercitados |
| Negación de booleano | Falta un test para el caso contrario |
| Cambio de constante | El valor exacto no está siendo verificado |
| Eliminación de línea (void call) | Efectos secundarios no testeados |
Priorizar mutantes a resolver
Ordenar por impacto:
- Mutantes en lógica de negocio crítica → prioridad máxima
- Mutantes en validaciones y condiciones de borde → alta prioridad
- Mutantes en código utilitario → media prioridad
- Mutantes en código de infraestructura/plumbing → baja prioridad
Para cada mutante sobreviviente importante, mostrar:
Archivo: src/services/PaymentService.ts:45
Mutante: ConditionalExpression → amount > 0 → amount >= 0
Estado: SOBREVIVIÓ ⚠️
Qué test agregarías: test que verifique que amount=0 es rechazado
Paso 5: Sugerir mejoras a los tests
Para cada mutante sobreviviente prioritario, proponer un test concreto:
Ejemplo para mutante de boundary condition
Mutante original: if (price > 0)
Mutante inyectado: if (price >= 0)
→ Agregar test: expect(validatePrice(0)).toBeFalsy()
Ejemplo para mutante de void call eliminado
Mutante: eliminar llamada a logger.audit(event)
→ Agregar test: expect(logger.audit).toHaveBeenCalledWith(expectedEvent)
Si el usuario lo pide, generar el código del test sugerido adaptado al framework de testing del proyecto (Jest, JUnit, pytest, RSpec, etc.).
Para identificar el framework:
jq '.devDependencies | keys | map(select(test("jest|vitest|mocha|jasmine")))' package.json
grep -E 'junit|testng|spock' pom.xml build.gradle
grep -E 'pytest|unittest|nose' requirements*.txt setup.cfg pyproject.toml
grep -E 'rspec|minitest' Gemfile
Paso 6: Seguimiento del score
Si el usuario quiere trackear la evolución del mutation score, sugerir:
echo "$(date +%Y-%m-%d): score=$(grep -oP 'Score: \K[\d.]+' mutation-report.txt)%" >> .mutation-history
Modos de uso
Modo "primera vez"
El usuario nunca corrió mutation testing. Ejecutar Pasos 0 → 1b → 2 (scope acotado) → 3 → dar una primera impresión del score y explicar qué significa.
Modo "análisis de huecos"
El usuario ya tiene resultados y quiere entender qué mejorar. Ejecutar Pasos 3 → 4 → 5. Foco en los mutantes sobrevivientes más impactantes.
Modo "mejorar score"
El usuario quiere subir el mutation score. Ejecutar Paso 4 en detalle, generar tests concretos del Paso 5, re-correr para verificar mejora.
Modo "integrar en CI"
El usuario quiere que mutation testing falle el pipeline si el score baja. Mostrar configuración de umbral de la herramienta correspondiente y el comando de CI.
Tips y edge cases
- Primera corrida lenta: en proyectos grandes, acotar con
--mutate o targetClasses a un módulo específico primero. Una vez que funciona, ampliar.
- Mutantes de timeout: generalmente indican loops que dependen del valor mutado. No son un problema crítico pero vale investigarlos.
- Falsos positivos: algunos mutantes sobreviven porque el comportamiento mutado es equivalente en la práctica. Marcarlos como "equivalentes" en la herramienta si aplica.
- Cobertura de línea vs mutation score: 100% de cobertura de líneas NO garantiza buen mutation score. Un test puede ejecutar código sin verificar su resultado.
- Velocidad:
coverageAnalysis: 'perTest' en Stryker y threads en Pitest aceleran la corrida significativamente.
- Exclude patterns: excluir código generado, DTOs sin lógica, y adaptadores de infraestructura del scope de mutation testing para reducir ruido.