| name | testing-mutation-stryker |
| description | Mutation testing con StrykerJS: verificar la calidad real de los tests. Stryker modifica el código (mutantes) y verifica que los tests detectan los cambios. Si un mutante sobrevive, el test suite es débil. |
| version | 1.0.0 |
| author | Mastermind |
Mutation Testing con StrykerJS
StrykerJS es el framework de mutation testing para JavaScript/TypeScript. En lugar de medir cobertura de líneas, mide si tus tests realmente detectan errores.
¿Qué es y por qué importa?
La cobertura de código mide qué líneas se ejecutan, no si se verifican. Puedes tener 100% de cobertura con tests que solo verifican toBeDefined().
Mutation testing crea versiones modificadas del código (mutantes) y verifica que los tests las detectan:
- Killed — el mutante fue detectado por los tests ✅
- Survived — el mutante pasó los tests ❌ (bug potencial)
- Timeout — el mutante hizo los tests demasiado lentos
- Errored — el mutante causó un error de compilación
Datos reales: StrykerJS tiene 2.9k+ estrellas en GitHub, soporta Jest, Mocha, Jasmine, Vitest, Karma, Tap, Cucumber.
Instalación
npm init stryker@latest
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
Configuración básica
El init genera stryker.config.mjs:
import { configureStryker } from '@stryker-mutator/core';
export default configureStryker((config) => {
config.testRunner = 'jest';
config.reporters = ['progress', 'clear-text'];
config.maxConcurrentRunners = 2;
config.coverageThreshold = {
auto: 'break',
low: 60,
high: 80,
};
config.mutator = '@stryker-mutator/js-mutator';
config.tsconfig = 'tsconfig.json';
});
Ejecución
npx stryker run
npx stryker run --logLevel trace
npx stryker run --incremental
npx stryker run --dashboard
Mutadores disponibles
Stryker aplica mutaciones automáticas al código:
| Mutador | Qué hace | Ejemplo |
|---|
ArithmeticOperator | Cambia operadores aritméticos | a + b → a - b |
BooleanLiteral | Invierte booleanos | true → false |
ConditionalExpression | Cambia condiciones | a ? b : c → a ? c : b |
EqualityOperator | Cambia comparaciones | === → !== |
ArrayMutation | Modifica arrays | arr[0] → arr[1] |
StringLiteral | Cambia strings | "hello" → "" |
BlockStatement | Elimina bloques | if (x) { return 1; } → return undefined |
Ejemplo: detectar tests débiles
export function calcularPrecioBase(precio, iva) {
return precio * (1 + iva / 100);
}
test('calcularPrecioBase returns something', () => {
expect(calcularPrecioBase(100, 21)).toBeDefined();
});
Resultado Stryker: El mutante precio * (1 + iva / 100) → precio * (1 - iva / 100) sobrevive porque el test solo verifica toBeDefined().
Test fuerte:
test('calcularPrecioBase calculates correctly', () => {
expect(calcularPrecioBase(100, 21)).toBe(121);
expect(calcularPrecioBase(100, 0)).toBe(100);
});
Ahora Stryker mata el mutante ✅.
Ejemplo práctico: conversión de unidades ESIOS
export function convertEsiosValue(raw, factor) {
if (factor === undefined || factor === null) return raw;
return Math.floor(raw / factor);
}
test('converts values', () => {
expect(convertEsiosValue(100, 10)).toBeDefined();
});
test('convertEsiosValue divides by factor', () => {
expect(convertEsiosValue(100, 10)).toBe(10);
expect(convertEsiosValue(0, 10)).toBe(0);
expect(convertEsiosValue(123, 10)).toBe(12);
expect(convertEsiosValue(100, undefined)).toBe(100);
});
Integración con CI/CD
name: Mutation Testing
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
mutation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Run Stryker
run: npx stryker run
env:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_API_KEY }}
STRYKER_DASHBOARD_PROJECT: mi-proyecto
STRYKER_DASHBOARD_REPO: github/Ntizar/mi-repo
Métricas y umbral
export default configureStryker((config) => {
config.coverageThreshold = 'break';
config.mutationScoreThreshold = 70;
config.reporters = [
'progress',
'clear-text',
'html',
'dashboard',
];
});
Mutation Score vs Coverage
| Métrica | Mide | Problema |
|---|
| Line coverage | % de líneas ejecutadas | Tests pueden no verificar nada |
| Branch coverage | % de caminos ejecutados | Tests pueden no verificar resultados |
| Mutation score | % de errores detectados | Mide la calidad real del test |
Regla práctica: Un mutation score de 80%+ es bueno. 90%+ es excelente. 50% significa que tus tests son débiles.
Pitfalls
- ❌ Stryker es lento — ejecuta los tests N veces (N = número de mutantes). Usar
--incremental y limitar en CI
- ❌ Mutantes que causan errores de compilación — Stryker los marca como "errored". No cuentan ni a favor ni en contra.
- ❌ Timeouts en mutantes — algunos mutantes crean bucles infinitos. Configurar
timeoutMS en stryker.config
- ❌ No confundir con cobertura — puedes tener 100% coverage y 0% mutation score
- ❌ No ejecutar en cada PR — mutation testing es caro. Usar en nightly o merge a main
- ❌ Ignorar mutantes en código de terceros — configurar
ignoreModules para node_modules
Estrategia de mejora
- Ejecutar Stryker → ver mutantes sobrevividos
- Analizar cada mutante sobrevivido → ¿por qué no lo detecta el test?
- Escribir un test más específico → mata el mutante
- Repetir → objetivo: mutation score > 70%
Referencias
Skills relacionadas
testing-jest-mocks-api — mocking de APIs externas con Jest
testing-property-based-fast-check — property-based testing con fast-check