Simulating Time — Testing the Behavior That Only Happens Later

Some of the most consequential code in any system runs when nobody is deploying: renewals at midnight, trials expiring on day 14, tokens timing out, retries backing off, data retention sweeping on schedule. Testing it by waiting is absurd; testing it by mocking one clock call is fragile; testing it properly means making time itself a controllable dependency of the system under test — and that choice, it turns out, improves the architecture even before it improves the tests.

Billing taught this series’ precursor lesson (bill-07): time-dominated systems are untestable until the clock is injected. This post generalizes it — the time-simulation patterns for any system where the calendar is a collaborator, and the architectural prerequisites they quietly enforce.

The clock as dependency, generalized

The foundational move everywhere: no code reads the wall clock directly. Every now(), every timer, every scheduled trigger flows through an injected clock interface — production binds real time; tests bind a controllable instant. What makes this generalize beyond billing is recognizing how many disguises time wears: explicit timestamps, yes, but also timeouts (a deadline is a clock read), retry backoff (hook-02’s schedules are time arithmetic), cache TTLs, rate-limit windows, token expiries, cron expressions, and “recent” anywhere in product logic. An honest audit of wall-clock reads is the first step and reliably shocking — dozens of hidden time dependencies per service, each one a behavior that currently can’t be tested deterministically.

Advancing simulated time must cause what real time would cause, and that requirement is the architectural teeth: timers and schedules registered with the clock fire when the test advances past them; scheduled jobs run through an executor the clock drives, not a system cron reading the wall. Systems refactored this way end up with explicit schedulers — every time-driven behavior declared and discoverable rather than buried in crontabs and sleeps — which is the same legibility win the subscription state machine got from scheduled transitions (bill-04): the test infrastructure forces the inventory that operations wanted anyway.

The patterns, by scope

Two companion disciplines round it out: time-zone and calendar edges are test fixtures, not folklore — DST transitions, month-length boundaries, leap days, and the user-in-Tokyo-billed-in-UTC cases live in a shared library of edge instants that time-dependent suites import (the golden-scenario pattern applied to the calendar); and real-time smoke remains — one thin layer of honest wall-clock tests (does the scheduler actually fire in production configuration?) guards the binding between the simulated world and the real one, in the non-blocking tier where its slowness belongs.

The takeaway: time becomes testable when it becomes a dependency — inject the clock everywhere (auditing time’s many disguises: timeouts, backoffs, TTLs, windows, expiries), drive all scheduled behavior through clock-driven executors (which forces the explicit-scheduler architecture operations wanted anyway), and simulate by scope: discrete-event logical clocks with boundary assertions in-process, one-clock slices or propagated test-time across services, and simulation-as-product-feature at the platform level. Keep a shared fixture library of calendar edges and a thin real-time smoke layer — and the code that runs “later” finally gets tested now.

Key takeaways

Further reading

Sources & References

executing a system by its events rather than its clock
the dependency being abstracted