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 decide qué hacer con el resultado, y decide si puedes confiar en una prueba que escribió un agente.
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. |
Cómo ejecutarlo de verdad
Las pruebas de mutación tienen fama de inasequibles, y casi siempre es un problema de alcance, no de herramientas. Este orden de operaciones las mantiene lo bastante baratas para sobrevivir al contacto con un calendario de entregas.
Primero, tener cobertura
- - La mutación sobre código sin cobertura solo informa de lo obvio, y a un coste alto
- - Usa la cobertura como barrera continua y barata, y reserva la mutación para código ya cubierto
- - Elige un módulo que importe —pagos, permisos, tarifas— en lugar de todo el repositorio
Acotar cada ejecución
- - El análisis de cobertura por prueba es la mayor palanca: solo ejecuta las pruebas que alcanzan cada mutante
- - En los pull requests, muta solo archivos cambiados, en modo incremental; eso es una ejecución de minutos
- - Mantén el barrido exhaustivo programado, fuera de la ruta crítica, donde una ejecución larga no le cuesta nada a nadie
Clasificar supervivientes en tres cestas
- - Un requisito que falta, que se convierte en una prueba positiva nueva nombrada por la regla
- - Un mutante equivalente que no cambia nada observable, que se documenta y excluye una vez
- - Código que ningún requisito pide, que se borra: la muerte más barata posible
Poner barrera a la regresión, no a un número
- - Fija un umbral break que impida que la puntuación se deslice, y párate ahí
- - Reporta los supervivientes como puntos de revisión y no como fallos de build, para que nadie aprenda a ignorar un build rojo
- - Sigue el tiempo de ejecución como métrica de primera clase; una barrera que se desactiva por lenta no protege nada
Qué cambia cuando el código lo escribe un LLM
Los agentes no crearon este problema, pero lo industrializaron. La distinción de polaridad deja de ser filosofía en el momento en que tus pruebas y tu implementación tienen el mismo autor y ese autor es un modelo.
Pide a un agente una funcionalidad con pruebas y obtendrás ambas, generadas desde una única lectura del requisito, en un único contexto. Si esa lectura era equivocada, las pruebas codifican el mismo malentendido y pasan, y la cobertura parece excelente, porque cada línea que escribió el modelo la ejercita una prueba que el modelo escribió para ejercitarla. Este es el fallo que la revisión ordinaria detecta peor, porque las pruebas parecen razonables en el diff. La mutación es la comprobación automática más barata que lo detecta, por una razón: no pregunta si existen pruebas, pregunta si reaccionan. Una prueba que refleja la implementación no notará que la implementación cambia.
Ponlo en el bucle, no en un informe
Un mutante superviviente es retroalimentación inusualmente buena para un agente: es ejecutable, específica e imposible de racionalizar. Un párrafo de consejo de revisión se acepta y se ignora; un cambio concreto que pasó desapercibido se arregla. Es exactamente la forma del rol hardener de SwarmForge, que ejecuta la herramienta de mutación archivo por archivo y no tiene permiso para avanzar hasta resolver los supervivientes.
Mueve el criterio a la instrucción de escritura
La mejor línea de ese proyecto está en su prompt del rol coder: escribe pruebas que fallarían ante una implementación equivocada plausible. Es un criterio de mutación incrustado en el paso de generación, mucho más barato que descubrir lo mismo en una auditoría una hora después. Pon esa frase en tus propias instrucciones de agente.
Separa al autor del endurecedor
El mismo modelo, en el mismo contexto, explicará por qué un superviviente está bien, porque el razonamiento que produjo el hueco sigue delante de él. Ejecuta la pasada de mutación como un paso aparte, con contexto aparte y sin acceso a la justificación original: solo el diff y la salida de la herramienta.
Espera caza de puntuación a velocidad de máquina
Si le pides subir la puntuación de mutación, un agente escribirá detectores de cambios mataMutantes más rápido de lo que nadie puede revisarlos. La ley de Goodhart es peor con agentes porque el volumen es gratis. Deja que los agentes reporten supervivientes y propongan pruebas a nivel de requisito; mantén a una persona, o a un rol de especificación, decidiendo cuál es el requisito.
Evaluar código que un agente acaba de producir
Cobertura y puntuación de mutación juntas son una rúbrica utilizable para trabajo escrito por IA. Léelas en pareja: la información interesante está en el desacuerdo.
| Señal | Qué suele significar | Qué hacer |
|---|---|---|
| Cobertura alta, puntuación de mutación baja | Las pruebas se escribieron para ejecutar el código, no para comprobarlo. La forma clásica de las pruebas generadas por un modelo. | Mantén la implementación en revisión, descarta o reescribe las pruebas a nivel de requisito. |
| Ambas altas, en un diff pequeño | Trabajo genuinamente bueno, o detectores de cambios bien disfrazados. | Revisa si hay spies, snapshots y aserciones de número de llamadas. Si las aserciones nombran reglas, adelante. |
| Supervivientes agrupados en rutas de error | El modelo implementó bien el camino feliz y narró el resto. | Especifica explícitamente el comportamiento ante fallos y pide pruebas sobre esa especificación. |
| Supervivientes en código que ningún requisito nombra | Generalidad especulativa: opciones, banderas y ramas defensivas que nadie pidió. | Borra el código. Es complejidad sin probar y sin dueño. |
| La puntuación subió y las pruebas tocan internos | El agente optimizó la métrica soldando la suite a la implementación de hoy. | Rechazar. El próximo refactor romperá estas pruebas sin que el comportamiento cambie. |
| La lista de exclusiones de mutantes equivalentes crece rápido | El agente está discutiendo con la herramienta en lugar de mejorar la suite. | Lee el diff tú mismo. Las exclusiones son una decisión humana, tomada una vez y con motivo registrado. |
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, incluidas las que un modelo se inventó para su propio código.
// 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. Un agente que optimiza la puntuación produce esta forma por defecto.
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 }
} La barrera para una rama escrita por un agente
shDos preguntas independientes, hechas por un paso que no escribió el código: ¿tiene este código una forma que permita cambiarlo, y reaccionan las pruebas que lo protegen cuando cambia? Las dos respuestas van al pull request, y ninguna es negociable por el autor.
CHANGED=$(git diff --name-only origin/main... -- 'src/**/*.js')
# structural: complexity against coverage on touched methods
crap-report --changed-only --threshold 30 $CHANGED || exit 1
# behavioural: do the new tests notice anything?
stryker run --incremental --mutate "$CHANGED"
# survivors are review items with a named requirement attached,
# never a licence to write a test that pins the call graph. 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.
Dejar que el autor endurezca su propio trabajo
Un modelo que escribió el código encontrará por qué cada superviviente es aceptable, porque su propio razonamiento le sigue pareciendo sólido. El endurecimiento tiene que ocurrir en un paso que solo vea el diff y la salida de la herramienta.
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.
Ese reparto importa más ahora que cuando ambos trabajos pertenecían al mismo ingeniero. Las pruebas deben seguir naciendo mediante TDD, como afirmaciones positivas sobre el comportamiento, incluso cuando las escribe un agente, y por eso la instrucción de escribir pruebas que fallarían ante una implementación equivocada plausible pertenece a tus prompts. Las ejecuciones de mutación auditan después esas afirmaciones desde un contexto aparte con el que no se puede discutir. 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 ambas prácticas ya están asignadas a roles propios en un enjambre de agentes en funcionamiento.
La métrica CRAP: hallar complejidad sin probar
Para qué sirve CRAP, cómo lo usa QA para dirigir pruebas y poner barreras de release, y cómo juzgar si un LLM escribe código mantenible.
SwarmForge a fondo: cómo funciona realmente el enjambre de Uncle Bob
Un análisis profundo de la tubería de roles, el demonio de traspasos y las barreras de calidad ejecutables de SwarmForge, con veredicto explícito sobre qué adoptar.
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, y como comprobación permanente de todo lo que haya escrito un agente.
¿Cómo ayudan las pruebas de mutación con código generado por IA?
Detectan el fallo concreto que la revisión pasa por alto: pruebas generadas desde el mismo malentendido que la implementación, que pasan y producen una cobertura excelente sin comprobar nada. A la mutación no le importa si las pruebas existen, solo si reaccionan a que el código cambie, así que una suite que refleja la implementación queda expuesta de inmediato.
¿Deberían los agentes ejecutar pruebas de mutación?
Sí, como paso separado del que escribió el código, y con los supervivientes tratados como preguntas y no como una puntuación a maximizar. Deja que el agente reporte supervivientes y proponga pruebas a nivel de requisito; mantén la decisión sobre cuál es el requisito en una persona o en un rol de especificación, o tendrás detectores de cambios a velocidad de máquina.
¿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.
¿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 en un pull request.
¿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.