Yeda AI Tips · #087

English

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:

  1. 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."
  2. 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.
  3. 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 deterministaReemplazo deterministaEjemplo en Python
Reloj del sistemaReloj inyectado/congeladofreezegun @freeze_time("2012-01-14"), o pasar un callable now
Sistema de archivosDirectorio temporal por testEl fixture tmp_path de pytest — un pathlib.Path único por test, con limpieza automática
AleatoriedadSemilla fija / RNG inyectadorandom.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

Recursos

¿Construyendo una función con IA? Yeda AI diseña, audita y entrega sistemas LLM de producción.

Habla con nosotros · Lee el blog · Read in English