Test intermitente: tu agente arregla un fallo que no existe
Un test intermitente (flaky test) es un test que, sin ningún cambio en el código, a veces pasa y a veces falla. Cuando tu agente de IA ve uno de esos fallos, suele tratarlo como un bug real: edita código, vuelve a lanzar el test, pasa por casualidad y te dice "arreglado". El cambio no ha arreglado nada, y además ahora está en tu diff.
El síntoma: el agente "arregla" y el test vuelve a fallar días después
La señal más clara es que el fallo reaparece sin que nadie haya tocado esa zona. Si reconoces alguno de estos patrones, sospecha de un test intermitente antes que de tu código:
- El agente dice que el test falló, cambia algo sin relación evidente con el mensaje de error y a la siguiente ejecución pasa.
- El test pasa cuando lo lanzas solo y falla cuando corre la suite completa.
- En tu máquina pasa siempre y en CI falla de vez en cuando.
- El diff del agente incluye un
sleep, un timeout más largo, un reintento o unskip"para estabilizar". - El mensaje de error habla de timeouts, conexiones rechazadas, fechas u orden de elementos en una lista.
Ninguno de estos síntomas prueba nada por sí solo. Lo que sí indican es que una ejecución no basta para saber si el cambio del agente sirvió.
Por qué un agente cae en esta trampa con tanta facilidad
El agente trata cada ejecución como una verdad, cuando en un test intermitente es solo una muestra. Su bucle típico es: ejecutar, ver rojo, editar, ejecutar, ver verde, terminar. Si el test falla, por ejemplo, una vez de cada diez, la ejecución posterior a casi cualquier edición saldrá verde, y el agente asocia ese verde a su cambio.
Además, las causas habituales de la intermitencia no se ven en el código del test. Un estudio empírico sobre tests intermitentes en JavaScript sitúa la concurrencia (esperas asíncronas, condiciones de carrera) como causa dominante, seguida de dependencias del sistema operativo y de la estabilidad de la red. Nada de eso aparece en el stack trace de forma obvia, así que el modelo rellena el hueco con la hipótesis más plausible y la "arregla".
El resultado esperado de ese bucle son parches que esconden el síntoma: esperas fijas, timeouts inflados o reintentos. El test pasa más veces, la causa sigue ahí y tu suite se vuelve más lenta.
Confírmalo repitiendo el test antes de tocar una línea
La forma más directa de confirmar la intermitencia es ejecutar el mismo test muchas veces sin cambiar nada. Si falla alguna vez, es intermitente; da igual lo que diga el agente sobre la causa.
En Python, el plugin pytest-repeat añade la opción --count, y combinado con -x se detiene en el primer fallo. Este comando repite un único test 50 veces:
pip install pytest-repeat
pytest "tests/test_pedidos.py::test_total_con_descuento" --count=50 -xEn Vitest, la opción --repeats de la CLI repite cada test el número de veces que indiques, independientemente del resultado:
npx vitest run src/pedidos.test.ts --repeats 50Jest no documenta un flag de repetición en su referencia de la CLI, así que puedes usar un bucle de shell. Este ejecuta el test filtrado por nombre hasta 50 veces y para en el primer fallo, indicando en qué intento ocurrió:
for i in $(seq 1 50); do
npx jest src/pedidos.test.ts -t "total con descuento" --silent \
|| { echo "Falla en el intento $i"; break; }
doneSobre cuántas repeticiones hacen falta, una heurística práctica, no un dato: 50 pasadas limpias bastan para descartar un fallo frecuente, pero no uno que aparece una vez cada mil. Si el test es barato, sube el número; pytest-repeat documenta precisamente el ejemplo --count=1000 -x para cazar fallos intermitentes.
Aísla la causa: orden, concurrencia o entorno
Una vez confirmado que es intermitente, cada experimento que añades descarta una familia de causas. La tabla es el núcleo del diagnóstico: ejecútala de arriba abajo y detente en la primera fila que reproduzca el fallo.
| Experimento | Si el fallo aparece aquí | Causa probable |
|---|---|---|
| Test aislado repetido N veces | Falla a veces incluso solo | Esperas asíncronas mal hechas, hora del sistema, datos aleatorios, red real |
| Test aislado pasa siempre, suite completa falla | Solo falla acompañado | Dependencia del orden: estado compartido, BD o ficheros que deja otro test |
| Suite en serie pasa, en paralelo falla | Solo con varios workers | Recursos compartidos: mismo puerto, misma BD de test, mismo directorio temporal |
| Local pasa siempre, CI falla a veces | Solo en otra máquina | Entorno: zona horaria, locale, SO, CPU más lenta, servicios externos |
Para el caso de orden en Python, pytest-randomly baraja módulos, clases y funciones, muestra la semilla al inicio de cada ejecución y permite reproducir el mismo orden:
# Reproduce el orden de la última ejecución
pytest --randomly-seed=last
# Desactiva el barajado para comparar
pytest -p no:randomlySi con el barajado desactivado el fallo desaparece y con la semilla guardada vuelve, tienes una dependencia de orden. Ojo: pytest-randomly también reinicia random.seed() antes de cada test, lo que puede ocultar o revelar fallos causados por datos aleatorios.
En Jest, --runInBand (alias -i) ejecuta todos los tests en serie en un único proceso, y --randomize --seed=1234 baraja los tests dentro de cada archivo con una semilla reproducible, según su documentación de la CLI. En Vitest, el equivalente es --sequence.shuffle.tests junto con --sequence.seed, que solo tiene efecto si el barajado está activo:
npx jest --runInBand
npx jest --randomize --seed=1234
npx vitest run --sequence.shuffle.tests --sequence.seed 1234Qué hacer según el resultado del diagnóstico
La regla general: el arreglo tiene que eliminar la no determinación, no tolerarla. Usa este árbol para decidir qué pedirle al agente:
- Pasa 50 de 50 o más y el fallo original era claro: no era intermitente. Trátalo como bug normal y exige al agente que explique la relación entre el error y su cambio.
- Falla solo aislado: pide que el test controle lo que no controla. Reloj inyectado o congelado en vez de la hora real, semilla fija para datos aleatorios, esperar la promesa o evento concreto en vez de un tiempo fijo, mock del servicio de red.
- Falla solo por orden: busca qué test deja estado sucio. La solución es aislar el estado con fixtures que se crean y destruyen por test, no reordenar la suite.
- Falla solo en paralelo: da a cada worker su propio recurso (puerto libre, base de datos o esquema por worker, directorio temporal único).
- Falla solo en CI: fija zona horaria y locale en la configuración de test y revisa si el test depende de velocidad de la máquina.
Los reintentos automáticos no son un arreglo, son una cuarentena. Herramientas como pytest-rerunfailures (--reruns N) o el reintento de Vitest (--retry, con opciones avanzadas desde la 4.1.0) sirven para que un fallo conocido no bloquee a todo el equipo mientras se arregla. Si los usas, deja una incidencia abierta y un comentario que explique por qué. El estudio citado antes encontró que más del 80 % de los tests intermitentes analizados se arreglaron eliminando la no determinación; el resto se saltaron, se pusieron en cuarentena o se borraron.
Revisa el diff del agente buscando parches de estabilidad
Antes de aceptar un "arreglo" de un test que fallaba, busca los parches típicos que ocultan intermitencia. Este comando lista las líneas añadidas en archivos de test o código que contienen patrones sospechosos, comparando con main:
git diff main -- '*test*' '*spec*' \
| grep -nE '^\+.*(sleep|setTimeout|waitForTimeout|retry|reruns|flaky|skip|xfail|timeout)'Cada coincidencia no es un error automático, pero sí una pregunta para el agente o para ti. Checklist de revisión:
- ¿Hay un
sleepo espera fija nueva? Pide que espere a la condición concreta. - ¿Ha subido un timeout? Pide la razón medible; "a veces tarda más" no lo es.
- ¿Ha añadido reintentos o marcadores
flaky? Solo con incidencia abierta. - ¿Ha marcado el test como
skipoxfail? Rechaza salvo que lo hayas decidido tú. - ¿Ha cambiado la aserción para aceptar más resultados (por ejemplo, ignorar el orden)? Solo es válido si el orden no forma parte del contrato.
- ¿El cambio en el código de producción tiene relación directa con el mensaje de error original? Si no la ves, pide que la explique.
Instrucciones para que tu agente no persiga fantasmas
Lo más eficaz es que el agente aplique este diagnóstico por sí mismo antes de editar. Puedes añadir un bloque como este a tu AGENTS.md, CLAUDE.md o archivo de reglas equivalente, adaptando los comandos a tu stack:
## Tests que fallan
- Si un test falla y el error no apunta claramente a tu cambio,
repítelo antes de editar: `pytest "RUTA::TEST" --count=30 -x`.
- Si pasa y falla sin cambios, es intermitente: NO edites código
de producción. Informa del ratio de fallos y de la causa probable.
- Nunca añadas sleep, timeouts mayores, reintentos, skip ni xfail
para que un test pase sin pedirme permiso explícito.
- Tras un arreglo, demuestra estabilidad: repite el test 30 veces
y muestra el resultado.Supongamos que el agente encuentra un fallo con este bloque en su contexto: lo esperado es que primero repita el test y te reporte "falla 3 de 30 aislado" en lugar de editar. Eso no garantiza que acierte la causa, pero cambia su bucle de "un verde y termino" a "demuestro estabilidad". Pedir la prueba de repetición al final es la parte que más protege tu diff.
Cuándo este diagnóstico no te sirve
No todo test rojo merece repetirse 50 veces. Sáltate este proceso cuando:
- El fallo es determinista y el mensaje es claro: una aserción que compara 10 con 12 justo después de que el agente tocara ese cálculo. Repetir solo gasta tiempo.
- La suite es muy lenta: repetir tests de integración de minutos 50 veces no es viable en local. Repite solo el test concreto, con menos iteraciones, o lanza la repetición en CI.
- El test depende de un servicio externo real: la intermitencia puede venir del servicio y no de tu código. Aquí el arreglo es aislar la dependencia con un mock o un contenedor, no investigar el orden.
- Trabajas con tests end-to-end de navegador: tienen sus propias herramientas de trazas y reintento; los flags de esta guía cubren tests unitarios y de integración con pytest, Jest y Vitest.
En esos casos, lo que sigue valiendo es la regla de revisión: si el diff del agente convierte un rojo en verde añadiendo esperas, reintentos o saltos, no lo aceptes sin entender por qué fallaba.
Preguntas frecuentes
¿Puedo dejar que el agente ejecute las repeticiones sin supervisión?
Sí, si tiene permiso para lanzar tests y el comando es de solo lectura sobre tu código. El riesgo está en la edición posterior, no en la repetición. Revisa el ratio de fallos que te reporte antes de aprobar cualquier cambio.
¿pytest-repeat y pytest-randomly se pueden usar juntos?
Son plugins independientes y cada uno documenta sus propias opciones. Si al combinarlos el comportamiento no es el esperado, desactiva el barajado con -p no:randomly mientras repites un test aislado y actívalo solo cuando investigues dependencias de orden.
¿Qué hago si el test solo falla en CI y no puedo reproducirlo?
Lanza la repetición en el propio CI con un job manual y guarda la semilla que imprimen pytest-randomly o Jest con --showSeed. Con la semilla puedes reproducir el mismo orden en local y comparar entornos (zona horaria, versión del runtime, número de workers).