¿Está seguro de que sus tests realmente funcionan?
\n\nImagine: tiene un 95% de cobertura de código, todos los tests están en verde, el CI/CD pasa sin errores. El equipo está contento, el jefe tranquilo. Pero un día aparece un error en producción. Abre el código, mira los tests y se da cuenta: simplemente verifican que la función devuelve un número, pero no miran su valor. O peor aún: el test está escrito de tal manera que pasa en cualquier escenario.
\n\n¿Le suena familiar? Bienvenido al mundo donde el porcentaje de cobertura es solo un número, no una garantía de calidad. Aquí entra en escena la prueba de mutación (mutation testing): una metodología que realmente hace sufrir a sus tests y los obliga a demostrar su validez.
\n\nEn este artículo, analizaremos cómo implementar mutation testing en un proyecto TypeScript usando Stryker, y por qué podría convertirse en su nueva herramienta favorita (o en una nueva fuente de dolores de cabeza).
\n\n¿Qué son las pruebas de mutación en palabras sencillas?
\n\nLas pruebas de mutación consisten en tomar su código, introducir deliberadamente pequeños errores (mutaciones) y ver si sus tests fallan. Si los tests no fallan, significa que está probando incorrectamente. Si fallan, bien hecho, pero el mutante ha sido eliminado.
\n\n¿Suena a sadismo? Quizás. Pero funciona. Imagine que es un ingeniero de QA que se ha colado en el código y ha cambiado === por ==, > por < o ha eliminado una llamada a una función. Un buen test debería detectarlo. Uno malo lo dejará pasar.
En TypeScript, con su tipado estricto, las pruebas de mutación son especialmente útiles: detectan casos donde los tipos son correctos, pero la lógica no.
\n\n¿Por qué la cobertura de código (code coverage) normal es una ilusión?
\n\nSupongamos que tiene una función:
\n\nfunction calculateDiscount(price: number, isPremium: boolean): number {\n if (isPremium) {\n return price * 0.8;\n }\n return price;\n}\n\nY un test:
\n\ntest('returns number', () => {\n const result = calculateDiscount(100, true);\n expect(typeof result).toBe('number');\n});\n\n¿Cobertura? 100%. ¿El test? Pasa. ¿Y si cambiamos price * 0.8 por price * 0.5? El test seguirá pasando: solo verifica el tipo, no el valor. He aquí una falsa sensación de seguridad.
Las pruebas de mutación resaltarán esa mutación y dirán: «Oye, tu test no notó que cambié el descuento». Y tendrá que escribir una verificación adecuada.
\n\nCómo funciona el mutation testing en TypeScript: paso a paso
\n\nTodo el proceso se puede dividir en tres etapas:
\n\n- \n
- Generación de mutantes — la herramienta copia su código y realiza un cambio a la vez. Por ejemplo, cambia
&&por||o elimina una llamada a un método. \n - Ejecución de tests — para cada mutante se ejecutan sus tests. \n
- Análisis de resultados — si los tests fallan, el mutante está «muerto». Si no, «sobrevive», y esa es una zona de riesgo. \n
La métrica principal es el mutation score (porcentaje de mutantes muertos). El 100% es el ideal, pero en la práctica un 80-90% ya es excelente.
\n\nHerramienta nº1 para TypeScript: Stryker
\n\nEn el mundo de TypeScript y JavaScript, el rey de las pruebas de mutación es Stryker (StrykerJS). Es rápido, funciona con Jest, Vitest, Mocha e incluso con tests de navegador. Se configura en 5 minutos.
\n\nInstalación:
\n\nnpm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner\n\nCreamos el archivo de configuración stryker.conf.js:
// stryker.conf.js\nexport default {\n mutate: ['src/**/*.ts', '!src/**/*.spec.ts'], // qué mutar\n testRunner: 'jest',\n reporters: ['html', 'clear-text', 'progress'],\n jest: {\n projectType: 'custom',\n configFile: 'jest.config.ts',\n },\n concurrency: 4, // cuántos mutantes