Yeda AI Tips · #074

Español

Inject Clocks, Random Seeds, and Temp Dirs

Your tests pass all day and fail at midnight. The bug is not your code — it is a hidden clock. Every time a test reads the real system clock, pulls from a globally seeded random generator, or writes into a shared temp directory, it takes an input you didn't pass and can't control. That is flakiness by design, and the fix is 30 years old: dependency injection. Pass in the clock, the seed, and the directory, and the test owns every input.

The three hidden inputs

A test is a function: same inputs, same result. These three sneak in through the back door:

Hidden inputHow it bitesInjected replacement
System clock Date-boundary logic flips at midnight, month-end, DST changes; "expires in 24h" assertions race the wall clock A clock object you can freeze — java.time.Clock.fixed(...), or a now() callable passed as a parameter
Global RNG A retry jitter or shuffled list differs per run; the failure reproduces once in fifty runs A seeded generator instance — random.Random(42) in Python — instead of module-level random.* calls
Shared temp dir Two tests (or two CI runs) collide on the same path; leftover files from a crashed run poison the next one A per-test unique directory — pytest's tmp_path fixture or tempfile.TemporaryDirectory()

The pattern is identical each time: the code under test stops calling out to the world and starts accepting the world as an argument.

The clock: wrap it, then freeze it

Martin Fowler's rule from Eradicating Non-Determinism in Tests: "Always wrap the system clock, so it can be easily substituted for testing." Java baked this into the standard library — the java.time.Clock docs explicitly say best practice is to pass a Clock into any method that needs the current instant, so a Clock.fixed(instant, zone) can be plugged in during tests. In Python or TypeScript the same idea is a one-line parameter:

def is_expired(token, now=None):
    now = now or datetime.now(timezone.utc)
    return token.expires_at <= now

# test — no midnight surprises:
assert is_expired(token, now=datetime(2026, 1, 1, tzinfo=timezone.utc))

Production callers pass nothing; tests pass a frozen instant. Zero mocking-framework gymnastics.

The seed: instances over globals

Python's own docs point the way: the module-level random.* functions are bound methods of one hidden shared instance, and "you can instantiate your own instances of Random to get generators that don't share state." Reusing a seed makes the sequence reproducible run to run. So give the code an RNG parameter (rng: random.Random), have production construct it once, and have tests pass random.Random(42). Now the "random" jitter, shuffle, or sample is a fixed, assertable sequence.

The temp dir: unique per test

Never hard-code /tmp/myapp. pytest's tmp_path fixture hands each test function its own unique directory; Python's tempfile.TemporaryDirectory() does the same as a context manager and removes the directory and its contents on exit. Parallel tests stop colliding, and a crashed run can't leave debris that fails the next one.

Why this matters more with AI agents

Deterministic tests are the feedback signal your coding agent iterates against. A flaky suite lies to the agent: it "fixes" failures that were never real and ships regressions that a rerun happened to hide. Design determinism in from the start — put "inject clocks, seeds, and temp dirs" in your project conventions file so generated code follows the pattern by default, instead of patching flakes after they land.

Power-user moves

Resources

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

Talk to us · Read the blog