Los tests inestables se deben al tiempo, al IO o a la aleatoriedad
Tu test no es inestable. Tu reloj lo es. Cuando un test pasa en una ejecución y falla en la siguiente — mismo código, misma entrada — el test casi nunca contiene un misterio. Contiene una dependencia oculta de algo que la ejecución no controla: el reloj del sistema, el sistema de archivos o un generador de números aleatorios. Esto aplica el doble a los tests generados por IA, que sin dudarlo usan datetime.now(), rutas fijas y aleatoriedad sin semilla, porque eso es lo que hace la mayoría del código del que aprendieron. La solución es la misma en todos los lenguajes: encuentra la entrada no determinista e inyéctala.
Los tres sospechosos de siempre
El catálogo clásico de no determinismo de Martin Fowler lista tiempo, aislamiento, esperas asíncronas, servicios remotos y fugas de recursos; en los tests unitarios del día a día, tres de esos dominan:
- El tiempo.
now()devuelve un valor distinto en cada ejecución. Cualquier aserción cerca de un límite — medianoche, fin de mes, una expiración de 30 días — falla en las ejecuciones que caen del lado equivocado. La regla de Fowler: "Always wrap the system clock, so it can be easily substituted for testing." - Sistema de archivos y estado compartido. Los tests que escriben en una ruta real y compartida chocan entre sí, con restos de ejecuciones anteriores y con workers en paralelo. El blog de testing de Google señala el estado compartido y el entorno como fuentes centrales de inestabilidad.
- La aleatoriedad. Un RNG sin semilla significa que cada ejecución ejercita una entrada distinta. El test no está verificando tu código — lo está muestreando.
Inyecta los tres
El mecanismo es inyección de dependencias, aplicada a lo aburrido:
| Entrada no determinista | Reemplazo determinista | Ejemplo en Python |
|---|---|---|
| Reloj del sistema | Reloj inyectado/congelado | freezegun @freeze_time("2012-01-14"), o pasar un callable now |
| Sistema de archivos | Directorio temporal por test | El fixture tmp_path de pytest — un pathlib.Path único por test, con limpieza automática |
| Aleatoriedad | Semilla fija / RNG inyectado | random.Random(42) — instancias con estado propio, sin globales |
from freezegun import freeze_time
import random
@freeze_time("2026-01-14")
def test_expiry(tmp_path):
rng = random.Random(42) # same sequence, every run
cache = Cache(dir=tmp_path, # unique dir per test
rng=rng,
ttl_days=30)
cache.put("k", "v")
assert not cache.expired("k") # clock is frozen — no midnight roulette
Misma entrada, mismo resultado, en cada ejecución. Nota el patrón: el código de producción recibe dir, rng y (implícitamente, vía freezegun) el reloj como entradas, en lugar de tomar globales. Ese es todo el truco.
Por qué los tests generados por IA fallan más
Los asistentes de código generan la versión estadísticamente más común de un test, y esa versión llama a datetime.now() directamente, escribe en /tmp/test.txt y llama a random.random() a secas. Nada de eso falla en la primera ejecución — falla en la 40, en CI, a las 11:59 PM. Así que revisa los tests generados por IA buscando exactamente esas tres llamadas antes de hacer merge. Un grep de 10 segundos — now(, random(, rutas fijas — atrapa la mayoría.
Trucos avanzados
- Haz del determinismo el valor por defecto, no el parche. Diseña constructores y funciones que acepten un reloj, un RNG y una ruta base con valores por defecto de producción (
now=datetime.now,rng=random,dir=None). Los tests inyectan; producción ni se entera. - Semilla por test, no por suite. Un
random.seed(42)a nivel de módulo se rompe en cuanto los tests corren en otro orden o en paralelo. Usa instanciasrandom.Random(42)— la documentación es explícita en que no comparten estado con el generador global. - Congela y luego avanza.
freezeguncongeladatetime.now(),time.time(),date.today()y compañía con un solo decorador; también puedes recibir el objeto congelado como parámetro y avanzarlo para probar que la expiración de verdad se dispara — el camino de fallo, no solo el feliz. - Cuarentena, no borrado. El consejo de Fowler para una suite ya inestable: mueve los tests inestables a un conjunto en cuarentena para que no envenenen la confianza en el build verde, y luego vacía la cuarentena con la lista de los tres sospechosos.
- Reproduce el fallo invirtiendo el truco. ¿No puedes reproducir un fallo intermitente? Fuerza las condiciones: pon el reloj a las 23:59:59, corre los tests en orden aleatorio, córrelos en paralelo. Un fallo que puedes reproducir a voluntad es solo un bug.
Recursos
- Eradicating Non-Determinism in Tests — Martin Fowler
- Flaky Tests at Google and How We Mitigate Them — Google Testing Blog
- Cómo usar directorios temporales en tests — docs de
tmp_pathde pytest - freezegun — congela
datetimeen tests de Python random—seed()e instanciasRandom(seed)— docs de Python
¿Construyendo una función con IA? Yeda AI diseña, audita y entrega sistemas LLM de producción.