How to mock date in jest

Call vi.setSystemTime(new Date('2026-02-01T00:00:00Z')) before the code runs, then assert against that instant. It freezes Date.now() and new Date() for the rest of the test, so an expiry assertion compares two known numbers instead of comparing the clock with itself and passing whatever the code does.

Why check this

Any assertion that reads the clock on both sides is unfalsifiable. expect(token.expiresAt).toBeGreaterThan(Date.now()) holds for a one hour token and for a one millisecond token, so it reports green on the defect it exists to catch. Freezing the date turns that comparison into arithmetic on a fixed number, and the test can fail again.

Run this before merging anything that computes an expiry, a renewal date, a retention window or an age. The failure it prevents: a session token whose lifetime is read in seconds and added in milliseconds, shipped because every clock assertion in the suite was true by construction.

Prerequisites

// billing.ts
/** ttlSeconds is seconds. The addition treats it as milliseconds. */
export function issueToken(ttlSeconds: number): { issuedAt: number; expiresAt: number } {
  const now = Date.now();
  return { issuedAt: now, expiresAt: now + ttlSeconds };
}

/** Same day next month, the way most billing code writes it. */
export function nextBillingDate(from: Date = new Date()): Date {
  const d = new Date(from);
  d.setMonth(d.getMonth() + 1);
  return d;
}

/** Tomorrow, computed by adding a day in milliseconds. */
export function tomorrow(from: Date = new Date()): Date {
  return new Date(from.getTime() + 86400000);
}

Steps

  1. Step 1.

    Freeze the clock and print what moved and what did not.

    // h-freeze.test.ts
    import { it, vi, afterEach } from 'vitest';
    
    afterEach(() => { vi.useRealTimers(); });
    
    it('setSystemTime without useFakeTimers', () => {
      const before = new Date().toISOString();
      vi.setSystemTime(new Date('2026-02-01T00:00:00Z'));
      console.log(JSON.stringify({
        before,
        after: new Date().toISOString(),
        dateNow: new Date(Date.now()).toISOString(),
      }, null, 2));
    });
    
    it('setSystemTime after useFakeTimers', () => {
      vi.useFakeTimers();
      vi.setSystemTime(new Date('2026-02-01T00:00:00Z'));
      const first = Date.now();
      const wall = performance.now();
      console.log(JSON.stringify({
        frozen: new Date().toISOString(),
        sameCallTwice: Date.now() - first,
        performanceNow: wall,
        parsedFromServer: new Date('2026-09-12T08:00:00Z').toISOString(),
        getSystemTime: vi.getMockedSystemTime()?.toISOString() ?? null,
      }, null, 2));
    });
    
    npx vitest run h-freeze.test.ts --reporter=verbose
    
     RUN  v5.0.0 D:/how2check/scratch/fake-timers-date-tz
    
    stdout | h-freeze.test.ts > setSystemTime without useFakeTimers
    {
    "before": "2026-09-12T08:05:46.141Z",
    "after": "2026-02-01T00:00:00.000Z",
    "dateNow": "2026-02-01T00:00:00.000Z"
    }
    
    stdout | h-freeze.test.ts > setSystemTime after useFakeTimers
    {
    "frozen": "2026-02-01T00:00:00.000Z",
    "sameCallTwice": 0,
    "performanceNow": 0,
    "parsedFromServer": "2026-09-12T08:00:00.000Z",
    "getSystemTime": "2026-02-01T00:00:00.000Z"
    }
    
    ✓ h-freeze.test.ts > setSystemTime without useFakeTimers 6ms
    ✓ h-freeze.test.ts > setSystemTime after useFakeTimers 2ms
    
    Test Files  1 passed (1)
        Tests  2 passed (2)
     Start at  11:05:45
     Duration  309ms (import 39%, transform 38%, tests 12%, worker 11%, environment 1%)

    Three facts in one block. Date.now() and new Date() both return the instant you set, and two reads in a row differ by zero. performance.now() restarts at 0. The parsed string keeps its own value, because a timestamp the server sent is data, not a clock reading, and freezing the clock never touches it.

  2. Step 2.

    Call setSystemTime on its own, with no fake timers, and check whether real timers still run.

    // i-ticking.test.ts
    import { it, vi, afterEach } from 'vitest';
    import { sleep } from './retry';
    
    afterEach(() => { vi.useRealTimers(); });
    
    it('does a bare setSystemTime freeze the clock or offset it', async () => {
      vi.setSystemTime(new Date('2026-02-01T00:00:00Z'));
      const t0 = Date.now();
      await sleep(250);
      const t1 = Date.now();
      console.log(JSON.stringify({
        firstRead: new Date(t0).toISOString(),
        afterRealSleep250ms: new Date(t1).toISOString(),
        movedMs: t1 - t0,
        realTimerStillRuns: true,
      }, null, 2));
    });
    
    it('and after useRealTimers', () => {
      console.log('now is', new Date().toISOString().slice(0, 10));
    });
    
    npx vitest run i-ticking.test.ts --reporter=verbose
    
     RUN  v5.0.0 D:/how2check/scratch/fake-timers-date-tz
    
    stdout | i-ticking.test.ts > does a bare setSystemTime freeze the clock or offset it
    {
    "firstRead": "2026-02-01T00:00:00.000Z",
    "afterRealSleep250ms": "2026-02-01T00:00:00.000Z",
    "movedMs": 0,
    "realTimerStillRuns": true
    }
    
    stdout | i-ticking.test.ts > and after useRealTimers
    now is 2026-09-12
    
    ✓ i-ticking.test.ts > does a bare setSystemTime freeze the clock or offset it 272ms
    ✓ i-ticking.test.ts > and after useRealTimers 1ms
    
    Test Files  1 passed (1)
        Tests  2 passed (2)
     Start at  11:06:00
     Duration  631ms (tests 77%, transform 12%, import 9%, worker 2%)

    The test really waited, 272 ms of wall clock, and the date did not move by a millisecond. setSystemTime alone replaces Date and leaves setTimeout real. That combination is what you want for a date assertion and the worst case for anything that measures its own duration, which now reads 0 for a wait that happened.

  3. Step 3.

    Assert the token expiry two ways: against the clock, and against the frozen instant.

    // j-token.test.ts
    import { it, expect, vi, afterEach } from 'vitest';
    import { issueToken } from './billing';
    
    const HOUR = 3600;
    afterEach(() => { vi.useRealTimers(); });
    
    it('token expires in the future', () => {
      const token = issueToken(HOUR);
      expect(token.expiresAt).toBeGreaterThan(Date.now());
    });
    
    it('token was issued no later than now', () => {
      const token = issueToken(HOUR);
      expect(token.issuedAt).toBeLessThanOrEqual(Date.now());
    });
    
    it('token expires one hour after a frozen now', () => {
      vi.setSystemTime(new Date('2026-02-01T00:00:00Z'));
      const fixed = Date.now();
      const token = issueToken(HOUR);
      expect(token.expiresAt).toBe(fixed + 60 * 60 * 1000);
    });
    
    npx vitest run j-token.test.ts
    
     RUN  v5.0.0 D:/how2check/scratch/fake-timers-date-tz
    
    ❯ j-token.test.ts (3 tests | 1 failed) 10ms
     × token expires one hour after a frozen now 7ms
    
    ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
    
    FAIL  j-token.test.ts > token expires one hour after a frozen now
    AssertionError: expected 1769904003600 to be 1769907600000 // Object.is equality
    
    - Expected
    + Received
    
    - 1769907600000
    + 1769904003600
    
    ❯ j-token.test.ts:21:27
       19|   const fixed = Date.now();
       20|   const token = issueToken(HOUR);
       21|   expect(token.expiresAt).toBe(fixed + 60 * 60 * 1000);
         |                           ^
       22| });
       23|
    
    ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
    
    
    Test Files  1 failed (1)
        Tests  1 failed | 2 passed (3)
     Start at  11:06:21
     Duration  299ms (import 41%, transform 37%, tests 13%, worker 8%)

    The two live-clock assertions pass on a token that expires 3.6 seconds after issue. The frozen one fails and prints the gap: 1769904003600 against 1769907600000, 3600 milliseconds added where 3600000 was meant. Neither of the first two can ever fail, whatever issueToken returns, as long as the number is positive.

  4. Step 4.

    Freeze at a calendar boundary and at a daylight saving transition, then let the code derive a date from it. The zone has to be named, so this run sets it in PowerShell.

    // m-offbyone.test.ts
    import { it, expect, vi, afterEach } from 'vitest';
    import { nextBillingDate, tomorrow } from './billing';
    
    afterEach(() => { vi.useRealTimers(); });
    
    it('a plan bought on 31 January renews in February', () => {
      vi.setSystemTime(new Date('2026-01-31T12:00:00Z'));
      const renewal = nextBillingDate();
      console.log('renewal:', renewal.toString().slice(0, 24));
      expect(renewal.getMonth()).toBe(1);
    });
    
    it('tomorrow keeps the same wall-clock hour', () => {
      vi.setSystemTime(new Date('2026-03-07T18:00:00Z'));
      const today = new Date();
      const next = tomorrow();
      console.log('today:', today.toString().slice(0, 33), '| tomorrow:', next.toString().slice(0, 33));
      expect(next.getHours()).toBe(today.getHours());
    });
    
    $env:TZ = 'America/New_York'; npx vitest run m-offbyone.test.ts --reporter=verbose
    
     RUN  v5.0.0 D:/how2check/scratch/fake-timers-date-tz
    
    stdout | m-offbyone.test.ts > a plan bought on 31 January renews in February
    renewal: Tue Mar 03 2026 07:00:00
    
    stdout | m-offbyone.test.ts > tomorrow keeps the same wall-clock hour
    today: Sat Mar 07 2026 13:00:00 GMT-0500 | tomorrow: Sun Mar 08 2026 14:00:00 GMT-0400
    
    × m-offbyone.test.ts > a plan bought on 31 January renews in February 18ms
     → expected 2 to be 1 // Object.is equality
    × m-offbyone.test.ts > tomorrow keeps the same wall-clock hour 2ms
     → expected 14 to be 13 // Object.is equality
    
    ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
    
    FAIL  m-offbyone.test.ts > a plan bought on 31 January renews in February
    AssertionError: expected 2 to be 1 // Object.is equality
    
    - Expected
    + Received
    
    - 1
    + 2
    
    ❯ m-offbyone.test.ts:10:30
        8|   const renewal = nextBillingDate();
        9|   console.log('renewal:', renewal.toString().slice(0, 24));
       10|   expect(renewal.getMonth()).toBe(1);
         |                              ^
       11| });
       12|
    
    ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
    
    FAIL  m-offbyone.test.ts > tomorrow keeps the same wall-clock hour
    AssertionError: expected 14 to be 13 // Object.is equality
    
    - Expected
    + Received
    
    - 13
    + 14
    
    ❯ m-offbyone.test.ts:18:27
       16|   const next = tomorrow();
       17|   console.log('today:', today.toString().slice(0, 33), '| tomorrow:', …
       18|   expect(next.getHours()).toBe(today.getHours());
         |                           ^
       19| });
       20|
    
    ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
    
    
    Test Files  1 failed (1)
        Tests  2 failed (2)
     Start at  04:09:15
     Duration  476ms (transform 41%, import 36%, tests 16%, worker 6%)

    setMonth(getMonth() + 1) on 31 January asks for 31 February, and the runtime rolls it forward to 3 March. Adding 86400000 milliseconds across the spring transition lands on the next calendar day at 14:00 instead of 13:00, because that local day was 23 hours long. Both defects are invisible on a date away from a boundary, which is where an unfrozen test lands almost every day of the year.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | The frozen assertion fails and the live one passes | The live assertion reads the clock on both sides | Keep the frozen one, delete the other | | Two reads of Date.now() differ by 0 | The clock is frozen, as intended | Advance it deliberately with vi.advanceTimersByTime | | A parsed timestamp keeps today's value | It came from a string, not from the clock | Fix the fixture, not the clock | | An elapsed-time value reads 0 after a real wait | setSystemTime froze Date while setTimeout stayed real | Install full fake timers, or stop measuring duration in that test | | A derived date lands on 3 March or an hour late | The boundary, not the mock, produced it | Keep the frozen date and fix the arithmetic |

Common mistakes

Sign: An assertion about expiry has never failed, on any branch, in any build.Cause: Both sides of the comparison read Date.now, so the assertion states that time moves forward rather than that the code is correct. It stays green when the unit is wrong, when the value is missing a zero, and when the addition is reversed.
Sign: The date is frozen and a duration measured by the code still reads 0.Cause: vi.setSystemTime without vi.useFakeTimers replaces Date and leaves setTimeout real, measured in step 2. The wait happens, the clock does not move, and anything the code computes from elapsed time is zero.
Sign: The suite is green in March and red on the first of the month.Cause: The frozen instant was picked as a round number in mid-month, so no test ever crosses a month end or a daylight saving transition. Pick 31 January and the local transition date on purpose, as step 4 does.
Sign: A test asserts on a timestamp the API returned and the mock makes no difference.Cause: Freezing the clock changes what the process reads, not what a fixture contains. A recorded response, a seeded database row and a hard-coded string all keep their own instant, which is correct and is often mistaken for the mock failing.

What to check next

FAQ

How do I freeze time in tests?

vi.setSystemTime(new Date('2026-02-01T00:00:00Z')), and vi.useRealTimers() in afterEach to undo it. Jest uses jest.setSystemTime with jest.useFakeTimers() first. Both freeze the instant rather than offsetting it, so a real wait inside the test moves the clock by zero.

Do I need useFakeTimers before setSystemTime?

Not in vitest 5. Step 2 calls setSystemTime alone and the date freezes while real timers keep firing. Call useFakeTimers when the code also waits on a timer, and leave it out when the code only reads a date and you want real waits to work.

How do I mock datetime now for code I do not own?

The same call covers it. A dependency reading Date.now() or new Date() inside the same process gets the frozen value, because the mock replaces the global rather than the import. Code reading time from an argument or from a response body needs a fixture instead.

Does freezing the clock change the timezone?

No. It sets the instant, and every local reading of that instant still goes through the machine zone, which is why step 4 has to name one. Run the same file under another zone to see which assertions move.

Why does performance.now return 0 after the freeze?

The fake clock owns performance.now as well and starts it at 0. Any code benchmarking itself with performance.now reports a duration of zero under fake timers, the same way Date.now arithmetic does.

Verified

Verified by Maks Vernyvitest 5.0.0node 22.23.2

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate10 minpublished updated Maks Verny