La generación de tests parece el trabajo perfecto para un asistente de IA: tedioso, basado en patrones y fácil de verificar. Pide a cualquier modelo actual que "escriba tests unitarios para esta clase" y en segundos tendrás un archivo ordenado con una docena de tests en verde. El problema es que los tests en verde no son el objetivo. Los tests existen para fallar cuando el código está mal, y los generados por IA a menudo no lo hacen. Esta guía explica por qué y cómo montar un flujo de trabajo en el que los tests generados aumenten de verdad tu tasa de detección de defectos y no solo tu cifra de cobertura.

El problema de fondo: tests que reflejan la implementación

Cuando un modelo genera tests leyendo la implementación, tiende a codificar lo que el código hace, no lo que debería hacer. Si la función tiene un bug, el test generado afirma el comportamiento defectuoso. La cobertura sube, la confianza sube y el bug queda protegido por un test.

Síntomas típicos de tests generados de poco valor:

  • Aserciones que repiten la implementación (expect(calc(2)).toBe(2 * RATE) copiado del código).
  • Mocking intensivo, de modo que el test solo comprueba que se llamó a los mocks en orden.
  • Solo caminos felices; ni límites, ni nulls, ni colecciones vacías, ni casos de error.
  • Tests de snapshot de objetos grandes que nadie lee.
  • Tests que pasan aunque borres el cuerpo del método.

El último síntoma se puede medir, y esa medición es la clave de todo el flujo de trabajo.

Lecciones de TestGen-LLM de Meta

Meta publicó uno de los informes industriales más instructivos sobre generación de tests con LLM. Su herramienta TestGen-LLM generaba tests adicionales para clases de test de Kotlin existentes en Instagram y Facebook, pero no se fiaba del modelo. Cada candidato tenía que superar una serie de filtros: debía compilar, debía pasar de forma fiable en varias ejecuciones (para eliminar tests inestables) y debía aumentar la cobertura. Solo entonces se proponía a los ingenieros como diff.

Las cifras son reveladoras. De los casos de test generados, alrededor del 75% compilaba correctamente, el 57% pasaba de forma fiable y el 25% aumentaba la cobertura. En los test-a-thons de Meta, los ingenieros aceptaron para producción el 73% de las mejoras recomendadas (Alshahwan et al., 2024).

La lección: la mayoría de las generaciones en bruto no son útiles, y no pasa nada, siempre que los filtros automáticos las descarten. Trata el modelo como un generador de candidatos y pon comprobaciones deterministas entre él y tu repositorio.

Mutation testing: el control de calidad que importa

La cobertura te dice qué líneas se ejecutaron. El mutation testing te dice si tus tests notarían que esas líneas están mal. Una herramienta de mutación hace pequeños cambios en tu código —cambiar > por >=, sustituir un valor de retorno, eliminar una llamada— y ejecuta los tests. Si los tests siguen pasando, el mutante "sobrevive" y has encontrado un hueco.

Hay herramientas maduras para la mayoría de los ecosistemas:

Lenguaje Herramienta
Java, Kotlin (JVM) PIT (pitest)
JavaScript, TypeScript, C#, Scala Stryker
Python mutmut, Cosmic Ray
Go go-mutesting, Gremlins

La puntuación de mutación es un objetivo mucho mejor que la cobertura para los tests generados con IA, porque es difícil de trampear: un test que no afirma nada no mata ningún mutante. La investigación sobre la combinación de LLM con mutation testing —devolver al modelo los mutantes supervivientes y pedirle un test que los mate— muestra mejoras claras en la detección de fallos frente a la generación guiada por cobertura, y hoy es un flujo de trabajo práctico que puedes ejecutar con cualquier agente.

Un flujo de trabajo que funciona

Paso 1: escribe la especificación antes que los tests

No pidas al modelo que "pruebe esta clase". Dale el comportamiento esperado:

Write JUnit 5 tests for DiscountService.calculate(order).

Business rules (source of truth — do NOT infer rules from the implementation):
- Orders >= 1000 UAH get 5% off; >= 5000 UAH get 10% off.
- Loyalty members get an extra 2%, applied after the volume discount.
- Discounts never exceed 15% in total.
- Orders with any item on promotion get no volume discount.
- Amounts are BigDecimal, rounded HALF_UP to 2 decimals.

Include boundary values (999.99, 1000.00, 4999.99, 5000.00),
an empty order and a null customer. Use AssertJ. No mocks for value objects.

Cuando la especificación y la implementación no coinciden, el test falla, que es exactamente lo que quieres. O el código tiene un bug o la especificación está desactualizada, y decide una persona.

Paso 2: ejecuta, filtra y quédate solo con los candidatos válidos

Automatiza los filtros de TestGen-LLM:

  1. Compila: si no, descártalo o devuelve el error del compilador una vez.
  2. Pasa de forma fiable: ejecútalo de tres a cinco veces y descarta los tests inestables.
  3. Falla por lo correcto: para cada test que falla, una persona comprueba si está mal el código o el test.

Paso 3: ejecuta mutation testing e itera sobre los supervivientes

# JVM: run PIT for one package
./gradlew pitest -Ppitest.targetClasses='com.shop.discount.*'

# TypeScript: run Stryker for one directory
npx stryker run --mutate "src/discount/**/*.ts"

Devuelve al agente los mutantes supervivientes: "Este mutante ha sobrevivido: en DiscountService.java:42, >= se cambió por >. Escribe un test que falle con el mutante y pase con el original". Repite hasta que la puntuación de mutación deje de mejorar o alcance tu umbral.

Paso 4: revisa como si fuera código de producción

Los tests generados pasan a formar parte del código y hay que mantenerlos. Comprueba la legibilidad, que los nombres describan el comportamiento (appliesTenPercentAtExactlyFiveThousand), que no haya exceso de mocks y que cada test tenga un único motivo para fallar.

Tests de caracterización para código heredado

En código heredado sin especificación, el problema de "reflejar la implementación" se convierte en una ventaja. Los tests de caracterización, un término de Michael Feathers, capturan deliberadamente el comportamiento actual —bugs incluidos— para poder refactorizar con seguridad. Los LLM son excelentes en esto:

  • Pide al agente que enumere las clases de entrada a partir de las ramas del código.
  • Genera tests que registren las salidas actuales, incluidas las sorprendentes.
  • Marca los resultados sospechosos con un comentario como // CHARACTERIZATION: looks like a bug, see #123.
  • Usa mutation testing para confirmar que la batería fija de verdad el comportamiento.

Es la base de cualquier migración o modernización; consulta IA para modernizar código heredado.

Prompts y patrones por stack

Java/Kotlin con Spring. Pide primero tests unitarios simples de las clases de dominio; usa slices @WebMvcTest o @DataJpaTest solo donde haga falta. Prohíbe explícitamente @SpringBootTest en tests unitarios: a los modelos les encanta y hace que las baterías sean lentas. Para Kotlin, especifica el framework (JUnit 5 + AssertJ, o Kotest) y MockK en lugar de Mockito.

TypeScript. Especifica Vitest o Jest, testing-library para componentes React y "prueba el comportamiento visible para el usuario, no los detalles de implementación". Pide tablas test.each para los valores límite: los modelos generan tablas legibles que cubren muchos casos a bajo coste.

Tests basados en propiedades. Pide al modelo que proponga invariantes ("el total nunca es negativo", "el descuento nunca supera el 15%") e impleméntalos con jqwik, el property testing de Kotest, fast-check o Hypothesis. Los LLM son buenos encontrando invariantes que las personas olvidan.

Generación agéntica de tests a escala

Con un agente de programación y un comando de test rápido, puedes ejecutar la generación de tests como tarea en segundo plano sobre un módulo entero: el agente elige la clase con menos cobertura, escribe tests a partir de una especificación o caracteriza el comportamiento, ejecuta los filtros y el mutation testing, y abre un PR pequeño por clase. Mantén los PRs pequeños y revisables, como se describe en buenas prácticas de agentic coding, y prográmalo fuera del horario laboral si tu capacidad de CI es limitada. Para ejecutar agentes en pipelines, consulta agentes de IA en CI/CD.

Integrar el control de calidad en CI

Los tests generados deben pasar por las mismas puertas que los filtros del generador cada vez que cambien. Una configuración práctica:

# .github/workflows/test-quality.yml (excerpt)
- name: Unit tests, three runs to detect flakiness
  run: for i in 1 2 3; do ./gradlew test --rerun-tasks || exit 1; done
- name: Mutation testing on changed modules
  run: ./gradlew pitest -Ppitest.mutationThreshold=70

Fija el umbral de mutación por módulo en lugar de globalmente: los módulos heredados pueden empezar en el 40% e ir subiendo, mientras que el código de dominio nuevo puede mantener un 75–80%. Ejecuta el mutation testing completo cada noche si es demasiado lento para cada pull request, y haz fallar el build solo ante descensos, no en módulos que todavía no han llegado al objetivo. Así la puntuación de mutación funciona como un trinquete: los tests generados solo pueden subirla.

Métricas que seguir

  • Puntuación de mutación de los módulos con tests generados con IA, antes y después.
  • Defectos escapados: bugs encontrados en producción en zonas con tests generados.
  • Tasa de tests inestables: los tests generados no deben hacer el CI menos fiable.
  • Duración de la batería de tests: vigila los tests de integración lentos disfrazados de unitarios.
  • Tiempo de revisión por PR de tests: si los revisores tardan más de lo que llevaría escribir los tests a mano, ajusta el flujo.

Evaluar el propio generador de tests es un pequeño problema de evals; el enfoque de evals de LLM se aplica directamente.

Preguntas frecuentes

¿Debería la IA generar tests para el código que acaba de escribir? Puede, pero los tests compartirán los malentendidos del código. Escribe o revisa tú la especificación, o genera los tests en una sesión limpia solo a partir de la especificación, sin mostrar la implementación.

¿Es el 100% de cobertura un buen objetivo para los tests generados? No. Fomenta tests de getters triviales y de código generado. Apunta a puntuaciones de mutación altas en la lógica de negocio.

¿Cómo evitamos tests frágiles? Prohíbe los mocks de objetos de valor y de colaboradores internos, prefiere probar a través de APIs públicas y rechaza los tests de snapshot salvo que la salida sea pequeña y significativa.

¿Funciona para tests de integración? Sí, sobre todo con Testcontainers y fixtures claros. Dale al agente la configuración de docker-compose y un test de ejemplo que funcione para que lo imite.

Fuentes

  1. Alshahwan et al. (2024). Automated Unit Test Improvement using Large Language Models at Meta.
  2. PIT mutation testing for the JVM.
  3. Stryker Mutator.
  4. Petrović, G., Ivanković, M. (2018). State of Mutation Testing at Google. ICSE-SEIP.
  5. Michael Feathers. Working Effectively with Legacy Code. Prentice Hall, 2004.
  6. Anthropic. Claude Code best practices: write tests, commit; code, iterate, commit.