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 input | How it bites | Injected 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
- Freeze time suite-wide. Libraries like freezegun (Python) or your framework's fake timers let you pin the clock for legacy code you can't refactor yet — but injected clocks stay the cleaner target.
- Print the seed on failure. If a test uses randomized data, derive the seed from the run and log it in the failure message so any red run replays exactly.
- Audit for the other hidden inputs. Environment variables, locale, timezone (
TZ), network, and dict/set iteration order are the same disease. Grep your suite fornow(,random., and/tmp/— each hit is a flake waiting for its midnight.
Resources
- Eradicating Non-Determinism in Tests — Martin Fowler
- java.time.Clock — designed for injectable, fixed clocks
random.Randomand seeding — Python docstmp_pathfixture — pytest how-totempfile.TemporaryDirectory— Python docs- Flaky Tests at Google and How We Mitigate Them — Google Testing Blog
Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.