Testing
Time travel
Code that depends on the time, such as an expiring link, a cache entry or a trial that ends after 30 days, is tested by moving the clock. The test case's time travel moves the clock your code reads, and puts it back after each test.
Introduction#
t.travel(5).days() moves the clock five days on, and t.travelTo(date) sets it to a date:
import { expect, test } from 'vitest'import { TestCase } from '../TestCase'const t = TestCase.boot()test('time can be manipulated', () => { t.travelTo(new Date('2026-01-01T00:00:00Z')) t.travel(5).days() expect(new Date().toISOString()).toBe('2026-01-06T00:00:00.000Z') t.travelBack()})While you travel, the clock stands still: each new Date() gives the same time until you travel again. travelBack()
returns to the real time, and the test case does it after each test.
Travelling#
travel(n) moves the clock by n of a unit, from where it stands, and a negative n moves it back:
t.travel(5).milliseconds()t.travel(5).seconds()t.travel(5).minutes()t.travel(5).hours()t.travel(5).days()t.travel(5).weeks()t.travel(5).months()t.travel(5).years()t.travel(-5).hours()Each unit has a singular form too, as t.travel(1).day(). Months and years overflow as Laravel's do: 31 January plus a
month is 3 March. The units count in UTC.
travelTo() takes a Date, an ISO string, or milliseconds since 1970:
t.travelTo('2026-03-04T10:00:00Z')Travelling for one function#
Give a function, and the clock travels while it runs. After it, or after its promise settles, the clock returns to
the real time, as travelBack() does, even when you had travelled before. The function's result is returned:
const user = await t.travelTo('2026-01-01T00:00:00Z', () => User.factory().create())await t.travel(5).days(async () => { // The clock is five days on here...})Freezing time#
freezeTime() holds the clock where it is now, and returns that time. freezeSecond() does the same at the start of
the current second, which helps when your database keeps seconds only:
const frozen = t.freezeTime()const second = t.freezeSecond()Both also take a function, and freeze the clock only while it runs, then return to the real time.
What the clock moves#
Time travel changes JavaScript's Date: new Date(), Date() and Date.now() read the travelled time, and so does
everything in your app that reads them:
now(), model timestamps and the dates your casts write- Expiry in the
array,fileanddatabasecache stores, cache locks and rate limiters - Signed URLs, password reset tokens, API tokens, and the expiry dates of the cookies your app sets
- Job delays and retry times
It doesn't change what runs outside JavaScript's Date:
- Timers:
setTimeout()andsetInterval()still wait in real time performance.now()- Redis, whose keys expire on the server's clock, so Redis cache entries and locks don't expire when you travel
- Your database's own clock:
CURRENT_TIMESTAMPandnow()in SQL
A test's session doesn't end either: the test client carries it from request to request, so travelling past
session.lifetime doesn't sign the user out.
Outside a test case#
travelTo(), travel(), travelBack(), freezeTime() and freezeSecond() are also functions, from
@orieljs/, for a test that doesn't boot the test case. They work the same, but nothing travels back
for you: call travelBack() in an afterEach(), or pass a function.
import { travelBack, travelTo } from '@orieljs/framework/testing'import { afterEach, expect, test } from 'vitest'afterEach(() => { travelBack()})test('the clock can be set', () => { travelTo('2026-01-01T00:00:00Z') expect(Date.now()).toBe(Date.parse('2026-01-01T00:00:00Z'))})