Yeda AI Tips · #087

Español

Flaky Tests Trace To Time, IO, Or Randomness

Your test isn't flaky. Your clock is. When a test passes on one run and fails on the next — same code, same input — the test almost never contains a mystery. It contains a hidden dependency on something the run doesn't control: the wall clock, the file system, or a random number generator. This is doubly true for AI-generated tests, which happily reach for datetime.now(), hard-coded paths, and unseeded randomness because that's what most training-set code does. The fix is the same in every language: find the nondeterministic input and inject it.

The three usual suspects

Martin Fowler's classic catalog of non-determinism lists time, isolation, async waits, remote services, and resource leaks; in day-to-day unit tests, three of those dominate:

  1. Time. now() returns a different value every run. Any assertion within a hair of a boundary — midnight, month end, a 30-day expiry — fails on the runs that land on the wrong side. Fowler's rule: "Always wrap the system clock, so it can be easily substituted for testing."
  2. File system and shared state. Tests that write to a real, shared path collide with each other, with leftovers from previous runs, and with parallel workers. Google's testing blog names shared state and environment as core flake sources.
  3. Randomness. An unseeded RNG means every run exercises a different input. The test isn't checking your code — it's sampling it.

Inject all three

The mechanism is dependency injection, applied to the boring stuff:

Nondeterministic inputDeterministic replacementPython example
Wall clockInjected/frozen clockfreezegun @freeze_time("2012-01-14"), or pass a now callable
File systemPer-test temp dirpytest's tmp_path fixture — a unique pathlib.Path per test, auto-cleaned
RandomnessFixed seed / injected RNGrandom.Random(42) — instances with their own state, no globals
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

Same input, same result, every single run. Note the pattern: the production code takes dir, rng, and (implicitly, via freezegun) the clock as inputs instead of reaching for globals. That's the whole trick.

Why AI-generated tests flake more

Code assistants generate the statistically common version of a test, and the statistically common version calls datetime.now() directly, writes to /tmp/test.txt, and calls random.random() bare. None of that fails on the first run — it fails on the 40th, in CI, at 11:59 PM. So review AI-generated tests for exactly these three calls before merging. A 10-second grep — now(, random(, hard-coded paths — catches most of it.

Power-user moves

Resources

Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.

Talk to us · Read the blog · Leer en español