How to Test Time-Dependent JavaScript Without Flaky Tests
Tests that depend on real time are often less reliable than they appear.
A test passes on your laptop, fails in CI, and succeeds again when you rerun it. Sometimes the problem isn't the application logic at all — it's the clock, a timer, or an assumption about how quickly an asynchronous operation will finish.
Time-dependent code appears everywhere: token expiration, retry delays, scheduled jobs, rate limits, subscription renewals, and cache expiration.
The solution is to make time controllable in tests.
The fragile approach
Imagine testing whether a temporary access token expires after five minutes:
const token = createToken();
await new Promise((resolve) => setTimeout(resolve, 300000));
expect(isExpired(token)).toBe(true);
This test takes five minutes to run. It can also behave unpredictably when the test environment is slow, and it makes the test suite unnecessarily expensive.
Adding a small sleep doesn't solve the underlying issue. It merely changes the timing assumptions.
Separate the clock from the business logic
A better approach is to inject a clock:
function isExpired(expiresAt, now = Date.now()) {
return now >= expiresAt;
}
const expiresAt = 1_800_000_300_000;
expect(isExpired(expiresAt, 1_800_000_000_000)).toBe(false);
expect(isExpired(expiresAt, 1_800_000_300_000)).toBe(true);
The tests can now provide an exact timestamp without waiting for the real clock to advance.
This pattern is especially useful when business rules depend on a specific instant rather than the passage of actual time.
What about timers?
For functions that use setTimeout() or setInterval(), use your test framework's fake timers.
For example, with Jest:
jest.useFakeTimers();
const callback = jest.fn();
setTimeout(callback, 1000);
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
jest.useRealTimers();
Fake timers let you advance scheduled time without making the test wait in real time.
The exact API depends on your testing framework. Be careful with asynchronous timers and promise scheduling: advancing a timer doesn't always mean every pending asynchronous operation has completed.
Three common mistakes
Using arbitrary sleeps. A fixed delay can make tests slow and still fail under load.
Ignoring time zones. Business dates and timestamps are different concepts. Test daylight-saving transitions and date boundaries when they affect the product.
Mixing wall-clock time with elapsed time. Use timestamps for calendar-based rules, but use a monotonic clock for measuring durations where possible. System clocks can jump.
A more reliable testing strategy
- Identify which parts of the code depend on the current time.
- Inject a clock or use fake timers.
- Test exact boundaries: just before expiration, at expiration, and just after it.
- Control asynchronous scheduling explicitly.
- Restore real timers and clean up after each test.
Make time a testable dependency
Time is often treated as a global constant that code can access whenever it needs to. In reality, it is an external dependency with edge cases of its own.
Once time can be controlled, tests become faster, more deterministic, and easier to reason about.
You don't need to wait five minutes to prove that five minutes have passed.
#JavaScript #NodeJS #Testing #SoftwareEngineering #Jest
About the author
- Shipmind Labs is a software development team helping startups, businesses, and agencies build and scale digital products. We specialize in web and mobile development, SaaS, AI solutions, Telegram bots, business automation, API integrations, and custom software. We can develop projects from scratch or join existing teams to provide reliable engineering support and deliver practical, scalable solutions.
Comments
Join the discussionNo comments yet. Be the first to comment.