Inyecta relojes, semillas aleatorias y directorios temporales
Tus tests pasan todo el día y fallan a medianoche. El bug no está en tu código: es un reloj oculto. Cada vez que un test lee el reloj real del sistema, consume un generador aleatorio global o escribe en un directorio temporal compartido, toma una entrada que tú no pasaste y no controlas. Eso es inestabilidad por diseño, y la solución tiene 30 años: inyección de dependencias. Pasa el reloj, la semilla y el directorio como parámetros, y el test controla todas sus entradas.
Las tres entradas ocultas
Un test es una función: mismas entradas, mismo resultado. Estas tres se cuelan por la puerta trasera:
| Entrada oculta | Cómo te afecta | Reemplazo inyectado |
|---|---|---|
| Reloj del sistema | La lógica de fechas cambia a medianoche, a fin de mes o con el horario de verano; las aserciones de "expira en 24h" compiten contra el reloj real | Un objeto reloj que puedes congelar — java.time.Clock.fixed(...), o un callable now() pasado como parámetro |
| RNG global | Un jitter de reintento o una lista mezclada difiere en cada corrida; el fallo se reproduce una vez de cada cincuenta | Una instancia con semilla — random.Random(42) en Python — en lugar de llamadas globales random.* |
| Dir temporal compartido | Dos tests (o dos corridas de CI) chocan en la misma ruta; archivos residuales de una corrida fallida contaminan la siguiente | Un directorio único por test — el fixture tmp_path de pytest o tempfile.TemporaryDirectory() |
El patrón es idéntico en los tres casos: el código bajo prueba deja de consultar al mundo y empieza a recibir el mundo como argumento.
El reloj: envuélvelo y luego congélalo
La regla de Martin Fowler en Eradicating Non-Determinism in Tests: "Always wrap the system clock, so it can be easily substituted for testing" (envuelve siempre el reloj del sistema para poder sustituirlo en los tests). Java lo incorporó a la biblioteca estándar: la documentación de java.time.Clock dice explícitamente que la mejor práctica es pasar un Clock a cualquier método que necesite el instante actual, para poder conectar un Clock.fixed(instant, zone) durante los tests. En Python o TypeScript, la misma idea es un parámetro de una línea:
def is_expired(token, now=None):
now = now or datetime.now(timezone.utc)
return token.expires_at <= now
# test — sin sorpresas a medianoche:
assert is_expired(token, now=datetime(2026, 1, 1, tzinfo=timezone.utc))
Los llamadores de producción no pasan nada; los tests pasan un instante congelado. Cero acrobacias con frameworks de mocking.
La semilla: instancias en vez de globales
La propia documentación de Python marca el camino: las funciones globales random.* son métodos de una única instancia compartida oculta, y "you can instantiate your own instances of Random to get generators that don't share state" (puedes crear tus propias instancias de Random para obtener generadores sin estado compartido). Reutilizar una semilla hace la secuencia reproducible entre corridas. Dale al código un parámetro RNG (rng: random.Random): producción lo construye una vez y los tests pasan random.Random(42). Ahora el jitter, la mezcla o el muestreo "aleatorio" es una secuencia fija y verificable.
El directorio temporal: único por test
Nunca escribas /tmp/myapp a mano. El fixture tmp_path de pytest le da a cada función de test su propio directorio único; tempfile.TemporaryDirectory() de Python hace lo mismo como context manager y elimina el directorio y su contenido al salir. Los tests en paralelo dejan de chocar, y una corrida que falló no puede dejar residuos que rompan la siguiente.
Por qué importa aún más con agentes de IA
Los tests deterministas son la señal de retroalimentación contra la que itera tu agente de código. Una suite inestable le miente al agente: "arregla" fallos que nunca fueron reales y publica regresiones que una nueva corrida ocultó por azar. Diseña el determinismo desde el principio: pon "inyecta relojes, semillas y directorios temporales" en el archivo de convenciones de tu proyecto, para que el código generado siga el patrón por defecto en vez de parchear la inestabilidad después.
Trucos avanzados
- Congela el tiempo en toda la suite. Bibliotecas como freezegun (Python) o los fake timers de tu framework fijan el reloj en código legado que aún no puedes refactorizar — pero el reloj inyectado sigue siendo el objetivo más limpio.
- Imprime la semilla al fallar. Si un test usa datos aleatorizados, deriva la semilla de la corrida y regístrala en el mensaje de fallo, para que cualquier corrida roja se reproduzca exactamente.
- Audita las otras entradas ocultas. Variables de entorno, locale, zona horaria (
TZ), red y el orden de iteración de dict/set son la misma enfermedad. Busca en tu suitenow(,random.y/tmp/— cada resultado es una falla esperando su medianoche.
Recursos
- Eradicating Non-Determinism in Tests — Martin Fowler
- java.time.Clock — diseñado para relojes inyectables y fijos
random.Randomy semillas — documentación de Python- Fixture
tmp_path— guía de pytest tempfile.TemporaryDirectory— documentación de Python- Flaky Tests at Google and How We Mitigate Them — Google Testing Blog
¿Construyes una función con IA? Yeda AI diseña, audita y entrega sistemas LLM de producción.