Mock the Current Time in Tests with Temporal
To make time-dependent code testable, accept the current instant as an injected value rather than calling Temporal.Now inside the logic. Part of Getting the Current Time with Temporal.Now.
Why this scenario is tricky
A test that reads the real clock behaves differently depending on when it runs, which is the opposite of what a test should do. An assertion about whether a token has expired, a subscription has lapsed, or "today" falls inside a promotion will pass at 2pm and fail at 11:59pm near a date boundary, and it will fail on a colleague's machine in another zone for reasons unrelated to the code. The root cause is that the code reaches out and pulls the current time from a global, so the test has no way to control it without reaching into that global too.
The failure is insidious because it is intermittent. A suite that reads the wall clock passes hundreds of times and then fails once, at a particular time of day or on a particular continent, and the failure has nothing to do with the change that triggered the build. Developers learn to re-run the pipeline until it goes green, which quietly trains the team to ignore red — the worst possible habit for a test suite to instil. Removing the dependency on the ambient clock removes the whole category of flake at its source.
The cure is to invert the direction of the dependency: instead of the logic pulling the time from Temporal.Now, the time is pushed into the logic as an ordinary input. Once "now" is a value the caller supplies rather than a global the function reaches for, a test can supply whatever moment it wants, and production can supply the real one. This is dependency injection applied to time, and it is the single idea that makes every time-dependent test deterministic.
Minimal working solution
The smallest version of the fix is a default parameter: a function that decides expiry accepts the current instant as an argument, defaulting to Temporal.Now.instant() so production callers need not pass anything, while a test passes a fixed instant. That one change turns an untestable clock read into a controllable input, and it makes the dependency on time visible in the signature rather than buried in the body — a reader can see at a glance that the function's result depends on the current moment.
import { Temporal } from '@js-temporal/polyfill';
// Accept 'now' as a parameter with a real-clock default.
function isExpired(token: { exp: Temporal.Instant }, now = Temporal.Now.instant()) {
return Temporal.Instant.compare(now, token.exp) >= 0;
}
// Test: isExpired(tok, Temporal.Instant.from('2026-01-01T00:00:00Z'))
The defaulted parameter is deliberately unobtrusive. Existing production call sites do not change at all, because they simply omit the argument and inherit the real clock; only the tests pass an explicit instant. That property — zero churn at the call sites, full control at the test — is what makes the pattern easy to adopt incrementally. You can convert one flaky function today without touching its callers, verify the test is now deterministic, and move on to the next one tomorrow.
Full production version
As a system grows, name the dependency with a small Clock interface — an object exposing now(). Production wires a systemClock that delegates to Temporal.Now.instant(); tests wire a fixedClock(instant) that always returns the same moment, or a controllable clock whose time you can advance. Passing the clock through the same dependency-injection boundary you already use for a database handle or HTTP client means every time-dependent unit becomes controllable from its edges, with no global to stub and nothing to tear down.
The Clock object scales better than a bare parameter once several functions in a call chain all need the time. Threading a raw instant through every intermediate function is tedious and couples each layer to the clock; passing one Clock dependency that the leaf functions read from keeps the plumbing in one place. The seam is uniform: freeze the clock to test a point in time, step it forward to test behaviour across an interval, and in production let it track the real wall clock.
import { Temporal } from '@js-temporal/polyfill';
export interface Clock { now(): Temporal.Instant; }
export const systemClock: Clock = { now: () => Temporal.Now.instant() };
export const fixedClock = (i: Temporal.Instant): Clock => ({ now: () => i });
// service(deps: { clock: Clock }) => deps.clock.now()
The reason to prefer injection over globally stubbing Date or Temporal.Now is isolation. A global stub is process-wide: it leaks into other tests running in the same worker, it has to be torn down in an afterEach that is easy to forget, and a missed teardown produces a flaky failure in a completely unrelated test that is maddening to trace. An injected clock is scoped to the code under test and disappears when the test function returns. It also documents intent — a reader of the function signature sees that it depends on the current time, which a hidden Temporal.Now call does not reveal.
The bottom line is that testable time is injected time. Make "now" an input — a defaulted parameter for small cases, a Clock object for larger ones — and your expiry checks, schedulers, and date-boundary logic become deterministic, fast, and free of the flakiness that global clock stubbing quietly introduces. Adopt the seam once and every future feature that touches time inherits it for free, which is why injecting the clock is one of the highest-leverage habits in a time-heavy codebase.
Verification snippet
The point of the exercise is that the test supplies the time, so the assertions become fully deterministic. Construct a fixed Temporal.Instant, pass it as the clock, and assert that a token expiring one second earlier reads as expired while one expiring one second later does not — both pinned to the instant you chose, with no dependence on when the suite runs.
There is a related discipline for time that advances during a test — simulating a countdown, or a series of events over minutes. Rather than sleeping, expose the clock so the test can step it forward: a controllable clock whose now() you can advance by a Temporal.Duration lets you assert behaviour at t, t+30s, and t+1h without any real waiting, keeping the suite fast and deterministic. The same injected-clock seam that freezes time also lets you fast-forward it, which is what makes testing schedulers, debouncers, and expiry logic tractable.
Common pitfalls
The main pitfall is calling Temporal.Now deep inside business logic, which leaves nothing for a test to control and forces global stubbing. The second is that global stubbing itself: patching Date or Temporal.Now process-wide leaks into other tests in the same worker and needs teardown that is easy to forget, producing flaky failures far from their cause. The third is threading a raw instant through dozens of call sites instead of a small Clock object, which becomes unwieldy — a named clock dependency injected at the boundary keeps the seam clean.
A subtler pitfall is injecting the clock but then still calling Temporal.Now in one forgotten branch — a fallback path, an error handler, a logging line. That single stray read reintroduces the non-determinism the injection was meant to remove, and because it is on a rarely-taken path it can hide for a long time. A quick lint rule that forbids Temporal.Now outside the systemClock definition catches these automatically, so the seam cannot silently spring a leak. Inject the clock, default it to the system clock in production, and freeze or advance it in tests.
Frequently Asked Questions
How do I freeze the current time in a unit test?
Stop calling Temporal.Now inside the logic and accept the current instant as an argument instead, defaulting to Temporal.Now.instant() in production. In the test, pass a fixed Temporal.Instant so the function's behaviour depends only on the value you supplied. No global patching is needed and the time dependency is visible in the signature.
Is injecting a clock better than stubbing Date globally?
Yes, in almost every case. A global stub is process-wide, leaks between tests in the same worker, and needs teardown that is easy to forget, producing flaky failures elsewhere. An injected Clock is scoped to the code under test, disappears when the test returns, and documents that the code depends on the current time.
How do I test logic that advances over time, like a countdown?
Inject a controllable clock whose now() you can step forward by a Temporal.Duration. The test advances the clock to t, t+30s, and t+1h and asserts at each point, with no real sleeping. The same seam that freezes time lets you fast-forward it, which is what makes schedulers and expiry logic deterministic to test.
Should I use a default parameter or a Clock object?
Use a default parameter for a single function or two — it is the least intrusive change and leaves existing callers untouched. Reach for a small Clock interface once several functions in a call chain all need the time, so you pass one dependency instead of threading a raw instant through every layer. Both approaches share the same core idea: now is an input, not a global.
How do I stop a stray Temporal.Now from reintroducing flakiness?
Add a lint rule that forbids Temporal.Now anywhere except the systemClock definition. That way a forgotten fallback path or logging line cannot quietly read the real clock and bring the non-determinism back. Every time read then flows through the injected clock, which tests fully control.