Testing Time-Dependent JavaScript with Jasmine's Mock Clock
Jasmine's built-in mock clock lets you test debounce, expiry, and polling logic deterministically — no real waiting, no flaky CI. Here's how install(), tick(), and mockDate() work, plus the uninstall and microtask pitfalls.
19 Jun 2026, 20:39 UTC

The test that takes 300 milliseconds too many
You wrote a debounce utility, a token-expiry check, or a retry-with-backoff loop, and now you need to test it. The naive approach — actually waiting with real setTimeout calls in your specs — makes the suite slow and, worse, flaky. A 300ms wait becomes 350ms on a loaded CI runner, and your assertion fires at the wrong moment.
Jasmine ships with a built-in answer: jasmine.clock(). It replaces the global timer functions (setTimeout, setInterval, and their clear counterparts) with mock implementations you control synchronously. Instead of waiting for time to pass, you advance it yourself with tick(milliseconds). Your spec runs in microseconds and behaves identically on every run.
Install, tick, uninstall
The mock clock is opt-in per spec or suite. The standard pattern is to install it in beforeEach and uninstall it in afterEach:
describe('debounce', () => {
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('invokes fn once after the quiet period', () => {
const fn = jasmine.createSpy('fn');
const debounced = debounce(fn, 300);
debounced();
expect(fn).not.toHaveBeenCalled();
jasmine.clock().tick(299);
expect(fn).not.toHaveBeenCalled();
jasmine.clock().tick(1);
expect(fn).toHaveBeenCalledTimes(1);
});
});Two mechanical details matter here. First, install the clock before the code under test captures the timer functions — if your module grabbed a reference to the real setTimeout at import time, the mock never gets a chance. Second, never skip uninstall(). This is the single most common mock-clock bug: the fake timers leak into subsequent specs, which then fail in confusing, order-dependent ways. If your suite suddenly passes alone but fails in full runs, a missing uninstall is the first thing to check.
Freezing \"now\" with mockDate
Timers handle elapsed time, but plenty of code reads the wall clock directly — think \"is this token past its expiresAt?\" Jasmine's mockDate() stubs the Date constructor so new Date() returns a fixed instant:
it('rejects an expired token', () => {
jasmine.clock().mockDate(new Date('2026-10-14T12:00:00Z'));
const token = { expiresAt: new Date('2026-10-14T11:59:59Z') };
expect(isValid(token)).toBe(false);
});This beats hand-rolling a Date monkey-patch because cleanup is handled by the same uninstall() call, and you avoid the classic mistake of forgetting to restore the global. Note that mockDate availability and behavior vary across Jasmine major versions — check the API docs for the jasmine-core version pinned in your package.json before relying on it.
Where the mock clock falls short
The mock clock controls timers, not the microtask queue. If your code does setTimeout(() => somePromise.then(...)), calling tick() fires the timer callback, but the Promise continuation still resolves on the real microtask queue. A spec mixing fake timers with async/await needs Jasmine's async spec support (an async spec function or a done callback) to flush those continuations — tick() alone won't do it.
There's a second boundary: the clock only fakes what it replaces. Real I/O, network requests, and (in browser environments) requestAnimationFrame live outside its control. Code that intertwines timers with those is a poor fit; the feature shines for pure, time-driven logic like debounce, throttle, polling intervals, and expiry checks. Also, don't combine Jasmine's clock with Sinon fake timers in the same suite — both stub the same globals, and double-stubbing produces genuinely weird failures.
Try it on your worst offender
Pick the slowest timer-dependent spec in your suite and convert it: install the clock, replace real waits with tick(), uninstall in afterEach. Then run the full suite twice — once normally, once with random spec ordering if your setup supports it. If the converted spec passes deterministically and the suite got faster, you've got the pattern right. If neighboring specs break, you almost certainly missed an uninstall() somewhere.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.