Las pruebas de mutación son negativas. Las de TDD, positivas.
A ambas se les llama pruebas, pero afirman cosas opuestas. Una prueba de TDD sostiene lo que el sistema debe hacer. Una ejecución de mutación solo puede informar de lo que tu suite no llegó a notar. Saber qué polaridad tienes en la mano cambia qué hacer con el resultado.
Dos tipos de afirmación
Una prueba guiada por pruebas se escribe antes de que exista el comportamiento. Falla, luego pasa, y desde entonces es una afirmación permanente: esta entrada debe producir este resultado. La suite se acumula en una especificación legible, y escribirla primero presiona el diseño, porque el código difícil de invocar es difícil de probar. Una ejecución de mutación hace algo estructuralmente distinto: toma código que ya funciona, lo cambia a propósito y vuelve a ejecutar la suite. Todo resultado está formulado en negativo: esta alteración pasó desapercibida. Nada en esa salida dice para qué sirve el software.
Una suite de TDD en verde es un conjunto de afirmaciones sobre intenciones. Una puntuación de mutación alta es la ausencia de un tipo concreto de evidencia contra tu suite. Solo una de esas dos cosas es una especificación.
La prueba positiva
- + Se escribe antes del código, así que el requisito existe en palabras antes de existir en lógica
- + Falla primero, que es la única prueba barata de que la prueba puede fallar
- + Nombra una regla, así que sobrevive al refactor: los internos pueden moverse sin que se mueva la aserción
- + Presiona el diseño mientras el diseño todavía es barato de cambiar
- + Se lee como documentación para la siguiente persona, incluido el siguiente agente
La comprobación negativa
- − Se ejecuta a posteriori, sobre código y pruebas que ya existen
- − Un mutante muerto no aporta información nueva; solo los supervivientes llevan señal
- − Un superviviente es evidencia de ceguera, nunca de un defecto del producto
- − No dice nada de requisitos que falten, solo de insensibilidad a los cambios que intentó
- − Produce un informe y una lista de tareas, no un artefacto que alguien conserve
La misma palabra, dos instrumentos distintos
| La prueba positiva | La comprobación negativa | |
|---|---|---|
| Qué afirma | El sistema debe comportarse así. | Tu suite no notó este cambio. |
| Qué prueba el verde | Todo comportamiento especificado hasta ahora está implementado. | La suite es sensible a ese conjunto de operadores. Nada sobre corrección. |
| Qué deja atrás | Pruebas y un diseño moldeado por haber tenido que escribirlas. | Un informe. Hay que extraer el valor antes de que caduque. |
| Cuándo se ejecuta | Continuamente, minuto a minuto, mientras se escribe el código. | En CI: incremental sobre archivos cambiados, o como barrido nocturno. |
| Modelo de coste | Se paga de forma sostenida y se amortiza en el desarrollo. | Mutantes por tiempo de las pruebas relevantes. Crece con ambos. |
| Efecto en el diseño | Presión de testabilidad. Un diseño incómodo duele de inmediato. | Ninguno. Califica lo que ya existe. |
| Cómo llega el fallo | En rojo, a propósito, escrito por ti, paso a paso. | Un superviviente que nadie escribió y que ahora hay que interpretar. |
El mismo superviviente, dos respuestas
Aquí la polaridad deja de ser filosofía. Un informe de mutación te da una ubicación; lo que escribas después decide si la suite se vuelve más fuerte o solo más rígida.
El código y el mutante que sobrevive
jsQuitar .trim() es una mutación de llamada a método habitual. Sobrevive siempre que ninguna fixture lleve espacios alrededor, lo que ocurre con la mayoría de las fixtures.
// importer.js
const normalise = (value) => value.trim().toLowerCase();
export function importRows(rows) {
return rows.map((row) => ({ email: normalise(row.email) }));
}
// Surviving mutant: normalise() with .trim() removed.
// The suite passes either way, so the report flags it. Matar al mutante y acoplar la suite
jsEsta prueba mata al superviviente. También congela la implementación actual: normalise tiene que seguir accesible y llamarse exactamente ese número de veces. Renómbrala, ponla en línea o muévela detrás de una frontera y la prueba se rompe sin que el comportamiento haya cambiado.
it('calls normalise once per row', () => {
const spy = vi.spyOn(internals, 'normalise');
importRows([{ email: ' Ada@Example.COM ' }]);
expect(spy).toHaveBeenCalledTimes(1);
});
// Green. Mutant dead. Score up.
// Nothing here states what an imported email address should look like. Responder a la pregunta que hizo el superviviente
jsEl mismo mutante, la misma muerte, pero la aserción es una regla que podría leer alguien de producto. Pasa por la función pública, así que los internos siguen libres de moverse, y dentro de seis meses se explica sola en el mensaje de fallo.
it('trims and lower-cases every imported email address', () => {
expect(importRows([{ email: ' Ada@Example.COM ' }]))
.toEqual([{ email: 'ada@example.com' }]);
});
// Same mutant dead, but the suite gained a specification
// instead of a snapshot of today's call graph. Mantener la auditoría lo bastante barata para seguir ejecutándola
jsonEl análisis de cobertura por prueba es la mayor palanca de rendimiento: solo ejecuta las pruebas que realmente alcanzan cada mutante. El modo incremental mantiene la ejecución de un pull request en minutos. El umbral break es un suelo contra la regresión, no un objetivo que perseguir.
{
"testRunner": "vitest",
"coverageAnalysis": "perTest",
"incremental": true,
"mutate": ["src/**/*.js", "!src/**/*.test.js"],
"thresholds": { "high": 80, "low": 60, "break": 60 }
} Dónde se vuelve tóxico lo negativo
Perseguir la puntuación
En cuanto la puntuación de mutación es un objetivo, se escriben pruebas para matar mutantes en lugar de enunciar requisitos. Pasan la revisión porque están en verde, y son justo las que se rompen en el siguiente refactor aunque el comportamiento no haya cambiado.
Pelearse con los mutantes equivalentes
Algunas mutaciones cambian el código sin cambiar el comportamiento observable. Ninguna prueba honesta puede matarlas. Documéntalas, exclúyelas y sigue. El tiempo invertido aquí compra un número, no confianza.
Afirmar la mutación en vez de la regla
Si no puedes nombrar el requisito que protege una prueba nueva, has escrito un detector de cambios. Señalará como fallo cada edición futura y enseñará al equipo a dejar de leer la salida de las pruebas.
Ejecutarlo todo, siempre
Un barrido completo en una base de código grande es un trabajo nocturno, no una barrera de pull request. Apunta la ejecución del PR a los archivos cambiados con análisis de cobertura por prueba y deja el barrido exhaustivo fuera de la ruta crítica.
Convertir cada superviviente en una afirmación positiva
Leer al superviviente como una pregunta
- - ¿Qué comportamiento tendría que ser cierto para que este cambio rompiera algo?
- - ¿Qué requisito, si alguien lo hubiera escrito, estaría fallando ahora mismo?
- - ¿Quién, más abajo, notaría si esta mutación llegara a producción?
Escribir la respuesta, no la muerte
- - Nombra la prueba por la regla, nunca por el mutante ni por el número de línea
- - Afirma a través de la superficie pública para que los internos sigan libres de cambiar
- - Si la regla solo se alcanza exponiendo los internos, eso es un hallazgo de diseño, no de pruebas
O borrar el código
- - Si no se puede nombrar ningún requisito, en realidad nada depende de ese comportamiento
- - Un superviviente en código sin especificar suele ser un requisito muerto antes que una prueba que falta
- - Borrarlo es la muerte más barata posible, y hace más rápida la siguiente auditoría
Una escribe la especificación, la otra la audita
Las pruebas de mutación no son un TDD mejor ni un sustituto suyo. No producen pruebas, ni diseño, ni declaración de intención. Lo que producen es una lista de lugares donde tu especificación es menos sensible de lo que suponías, y eso es genuinamente valioso y genuinamente distinto.
Así que mantén explícito el reparto. Las pruebas nacen mediante TDD, como afirmaciones positivas sobre el comportamiento. Las ejecuciones de mutación ocurren periódicamente como auditoría de esas afirmaciones. Y ningún superviviente pasa directo a una prueba: traduce primero a requisito, o borra el código donde vive. Un hallazgo negativo solo se convierte en valor duradero cuando alguien lo transforma en una afirmación positiva sobre para qué sirve el software.
Artículos de ingeniería relacionados
La cobertura y la complejidad tienen su propia métrica, y ya existe un flujo con agentes que asigna el endurecimiento a un rol propio.
La métrica CRAP: complejidad y cobertura en un número
Cómo la fórmula CRAP combina complejidad con cobertura, qué exige cada puntuación y cómo usarla como barrera sin abrir una épica de limpieza.
SwarmForge: lo que acierta el enjambre de agentes de Uncle Bob
Una revisión de la descomposición en roles, el aislamiento por worktree y el protocolo de traspaso de SwarmForge, y de qué partes sirven en cualquier equipo.
Preguntas frecuentes
¿Son las pruebas de mutación mejores que la cobertura de código?
Responden a una pregunta más fuerte a un precio mucho mayor. La cobertura cuenta si una línea se ejecutó; la mutación pregunta si algo llegó a afirmarse. Usa la cobertura como barrera continua y barata, y las pruebas de mutación como auditoría periódica del código que importa.
¿Qué puntuación de mutación deberíamos buscar?
Por encima de un 80 % se suele considerar sólido y entre 60 y 80 % aceptable con huecos reales, pero el número solo significa algo por módulo. Una regla más útil es que no haya regresión en los archivos cambiados, con un umbral break que evite que la puntuación se deslice.
¿Reemplazan las pruebas de mutación al TDD?
No. Las pruebas de mutación no escriben nada ni especifican nada. Califican la suite que ya tienes, y cada hallazgo sigue necesitando una decisión humana sobre qué requisito implica.
¿Qué herramientas usan los equipos?
PIT es la opción establecida en la JVM, Stryker Mutator cubre JavaScript, TypeScript, C# y Scala, y en Python las opciones habituales son mutmut y cosmic-ray. Todas permiten limitar una ejecución a los archivos cambiados, que es lo que hace asequible la práctica.
¿Dónde encajan las pruebas de mutación en CI?
De forma incremental en los pull requests, limitadas a archivos cambiados con análisis de cobertura por prueba, más un barrido completo programado. Reporta los supervivientes como puntos de revisión en lugar de fallos de build y bloquea solo si la puntuación retrocede.