edgegoat Guides

How to mock Date in JavaScript tests

Code that reads the current time is untestable until you control the clock. You do not always need fake timers: often you only need new Date() to return one specific edge instant.

Freeze the clock for one function call

example.js
import { withFrozenTime } from 'edgegoat';

withFrozenTime('2026-12-31T23:59:59.999Z', () => {
  Date.now();         // 1798761599999
  new Date();         // 2026-12-31T23:59:59.999Z
  new Date(2020, 0);  // still works normally
});

It accepts a Date, an ISO string or a timestamp, works with any test runner, restores the real Date afterwards (also when the function throws or its promise rejects) and leaves timers alone.

The bug: "is this today?" in UTC

example.js
function isToday(date) {
  return date.toISOString().slice(0, 10) === new Date().toISOString().slice(0, 10);
}

It compares UTC calendar days. At 23:30 in New York, 00:30 the same evening is "yesterday" in UTC terms. Test it with the clock frozen late in the evening, in every zone from timeZones():

example.test.js
import { expect, test } from 'vitest';
import { timeZones, withFrozenTime, withTimeZone } from 'edgegoat';

test.each(timeZones())('$label', ({ value: zone }) => {
  withTimeZone(zone, () => {
    const now = new Date(2026, 11, 31, 23, 30);
    const earlierToday = new Date(2026, 11, 31, 0, 30);
    withFrozenTime(now, () => expect(isToday(earlierToday)).toBe(true));
  });
});
Fails: every zone with a non zero offset at that moment, from Pacific/Pago_Pago to Pacific/Kiritimati. Only UTC-like zones pass. The fix compares local fields or formats both dates with Intl.DateTimeFormat in the user's zone.

How it works, and its limits

withFrozenTime swaps globalThis.Date for a thin wrapper whose prototype is the real one, so instanceof Date, Date.UTC, Date.parse, subclasses and new Date(value) keep working. Only Date.now(), new Date() and Date() are frozen.

Code that captured Date or Date.now before the call still sees real time, and performance.now() and timers are not touched. When you need to advance time or run timers, use your runner's fake timers instead.

When to use fake timers instead

  • Jest: jest.useFakeTimers({ now: date }) or jest.setSystemTime(date).
  • Vitest: vi.useFakeTimers() then vi.setSystemTime(date).
  • node:test: mock.timers in recent Node.js versions, including the Date API.

Fake timers are the right tool for debounce, intervals and timeouts. For "what does this function do at 23:59:59.999 on New Year's Eve", a frozen clock is simpler.

FAQ

Use jest.useFakeTimers({ now: date }), or withFrozenTime(date, fn) from edgegoat if you only need a fixed "now" without fake timers.

vi.setSystemTime(date) with fake timers enabled. vi.spyOn(Date, 'now') only covers Date.now(), while withFrozenTime also freezes new Date().

No. It only replaces Date. Timers, performance.now() and the event loop run normally.

Yes. Nest them: withTimeZone(zone, () => withFrozenTime(date, fn)). Both restore their state when the function returns.

More guides