Skip to main content
Home
This release is 3 versions behind 1.0.16 — the latest version of @std/testing. Jump to latest

@std/testing@1.0.13
Built and signed on GitHub Actions

Tools for testing Deno code like snapshot testing, bdd testing, and time mocking

This package works with Deno
This package works with Deno
JSR Score
94%
Published
7 months ago (1.0.13)
Package root>time.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
// Copyright 2018-2025 the Deno authors. MIT license. /** * Utilities for mocking time while testing. * * ```ts * import { * assertSpyCalls, * spy, * } from "@std/testing/mock"; * import { FakeTime } from "@std/testing/time"; * * function secondInterval(cb: () => void): number { * return setInterval(cb, 1000); * } * * Deno.test("secondInterval calls callback every second and stops after being cleared", () => { * using time = new FakeTime(); * * const cb = spy(); * const intervalId = secondInterval(cb); * assertSpyCalls(cb, 0); * time.tick(500); * assertSpyCalls(cb, 0); * time.tick(500); * assertSpyCalls(cb, 1); * time.tick(3500); * assertSpyCalls(cb, 4); * * clearInterval(intervalId); * time.tick(1000); * assertSpyCalls(cb, 4); * }); * ``` * * @module */ import { RedBlackTree } from "jsr:@std/data-structures@^1.0.8/red-black-tree"; import { ascend } from "jsr:@std/data-structures@^1.0.8/comparators"; import type { DelayOptions } from "jsr:@std/async@^1.0.13/delay"; import { _internals } from "./_time.ts"; export type { DelayOptions }; /** * Represents an error when trying to execute an invalid operation on fake time, * given the state fake time is in. * * @example Usage * ```ts * import { FakeTime, TimeError } from "@std/testing/time"; * import { assertThrows } from "@std/assert"; * * assertThrows(() => { * const time = new FakeTime(); * time.restore(); * time.restore(); * }, TimeError); * ``` */ export class TimeError extends Error { /** Construct TimeError. * * @param message The error message */ constructor(message: string) { super(message); this.name = "TimeError"; } } function fakeTimeNow() { return time?.now ?? _internals.Date.now(); } const FakeDate = new Proxy(Date, { construct(_target, args) { if (args.length === 0) args.push(FakeDate.now()); // @ts-expect-error this is a passthrough return new _internals.Date(...args); }, apply(_target, _thisArg, _args) { return new _internals.Date(fakeTimeNow()).toString(); }, get(target, prop, receiver) { if (prop === "now") { return fakeTimeNow; } return Reflect.get(target, prop, receiver); }, }); interface Timer { id: number; // deno-lint-ignore no-explicit-any callback: (...args: any[]) => void; delay: number; args: unknown[]; due: number; repeat: boolean; } /** The option for {@linkcode FakeTime} */ export interface FakeTimeOptions { /** * The rate relative to real time at which fake time is updated. * By default time only moves forward through calling tick or setting now. * Set to 1 to have the fake time automatically tick forward at the same rate in milliseconds as real time. * * @default {0} */ advanceRate: number; /** * The frequency in milliseconds at which fake time is updated. * If advanceRate is set, we will update the time every 10 milliseconds by default. * * @default {10} */ advanceFrequency?: number; } interface DueNode { due: number; timers: Timer[]; } let time: FakeTime | undefined = undefined; function fakeSetTimeout( // deno-lint-ignore no-explicit-any callback: (...args: any[]) => void, delay = 0, // deno-lint-ignore no-explicit-any ...args: any[] ): number { if (!time) throw new TimeError("Cannot set timeout: time is not faked"); return setTimer(callback, delay, args, false); } function fakeClearTimeout(id?: unknown) { if (!time) throw new TimeError("Cannot clear timeout: time is not faked"); if (typeof id === "number" && dueNodes.has(id)) { dueNodes.delete(id); } } function fakeSetInterval( // deno-lint-ignore no-explicit-any callback: (...args: any[]) => unknown, delay = 0, // deno-lint-ignore no-explicit-any ...args: any[] ): number { if (!time) throw new TimeError("Cannot set interval: time is not faked"); return setTimer(callback, delay, args, true); } function fakeClearInterval(id?: unknown) { if (!time) throw new TimeError("Cannot clear interval: time is not faked"); if (typeof id === "number" && dueNodes.has(id)) { dueNodes.delete(id); } } function setTimer( // deno-lint-ignore no-explicit-any callback: (...args: any[]) => void, delay = 0, args: unknown[], repeat = false, ): number { const id: number = timerId.next().value; delay = Math.max(repeat ? 1 : 0, Math.floor(delay)); const due: number = now + delay; let dueNode: DueNode | null = dueTree.find({ due } as DueNode); if (dueNode === null) { dueNode = { due, timers: [] }; dueTree.insert(dueNode); } dueNode.timers.push({ id, callback, args, delay, due, repeat, }); dueNodes.set(id, dueNode); return id; } function fakeAbortSignalTimeout(delay: number): AbortSignal { const aborter = new AbortController(); fakeSetTimeout(() => { aborter.abort(new DOMException("Signal timed out.", "TimeoutError")); }, delay); return aborter.signal; } function overrideGlobals() { globalThis.Date = FakeDate; globalThis.setTimeout = fakeSetTimeout; globalThis.clearTimeout = fakeClearTimeout; globalThis.setInterval = fakeSetInterval; globalThis.clearInterval = fakeClearInterval; AbortSignal.timeout = fakeAbortSignalTimeout; } function restoreGlobals() { globalThis.Date = _internals.Date; globalThis.setTimeout = _internals.setTimeout; globalThis.clearTimeout = _internals.clearTimeout; globalThis.setInterval = _internals.setInterval; globalThis.clearInterval = _internals.clearInterval; AbortSignal.timeout = _internals.AbortSignalTimeout; } function* timerIdGen() { let i = 1; while (true) yield i++; } function nextDueNode(): DueNode | null { for (;;) { const dueNode = dueTree.min(); if (!dueNode) return null; const hasTimer = dueNode.timers.some((timer) => dueNodes.has(timer.id)); if (hasTimer) return dueNode; dueTree.remove(dueNode); } } let startedAt: number; let now: number; let initializedAt: number; let advanceRate: number; let advanceFrequency: number; let advanceIntervalId: number | undefined; let timerId: Generator<number>; let dueNodes: Map<number, DueNode>; let dueTree: RedBlackTree<DueNode>; /** * Overrides the real Date object and timer functions with fake ones that can be * controlled through the fake time instance. * * Note: there is no setter for the `start` property, as it cannot be changed * after initialization. * * @example Usage * ```ts * import { * assertSpyCalls, * spy, * } from "@std/testing/mock"; * import { FakeTime } from "@std/testing/time"; * * function secondInterval(cb: () => void): number { * return setInterval(cb, 1000); * } * * Deno.test("secondInterval calls callback every second and stops after being cleared", () => { * using time = new FakeTime(); * * const cb = spy(); * const intervalId = secondInterval(cb); * assertSpyCalls(cb, 0); * time.tick(500); * assertSpyCalls(cb, 0); * time.tick(500); * assertSpyCalls(cb, 1); * time.tick(3500); * assertSpyCalls(cb, 4); * * clearInterval(intervalId); * time.tick(1000); * assertSpyCalls(cb, 4); * }); * ``` */ export class FakeTime { /** * Construct a FakeTime object. This overrides the real Date object and timer functions with fake ones that can be * controlled through the fake time instance. * * @param start The time to simulate. The default is the current time. * @param options The options * * @throws {TimeError} If time is already faked * @throws {TypeError} If the start is invalid */ constructor( start?: number | string | Date | null, options?: FakeTimeOptions, ) { if (time) { throw new TimeError("Cannot construct FakeTime: time is already faked"); } initializedAt = _internals.Date.now(); startedAt = start instanceof Date ? start.valueOf() : typeof start === "number" ? Math.floor(start) : typeof start === "string" ? (new Date(start)).valueOf() : initializedAt; if (Number.isNaN(startedAt)) { throw new TypeError( `Cannot construct FakeTime: invalid start time ${startedAt}`, ); } now = startedAt; timerId = timerIdGen(); dueNodes = new Map(); dueTree = new RedBlackTree( (a: DueNode, b: DueNode) => ascend(a.due, b.due), ); overrideGlobals(); time = this; advanceRate = Math.max( 0, options?.advanceRate ?? 0, ); advanceFrequency = Math.max( 0, options?.advanceFrequency ?? 10, ); advanceIntervalId = advanceRate > 0 ? _internals.setInterval.call(null, () => { this.tick(advanceRate * advanceFrequency); }, advanceFrequency) : undefined; } /** * Restores real time. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assertEquals, assertNotEquals } from "@std/assert"; * * const setTimeout = globalThis.setTimeout; * * { * using fakeTime = new FakeTime(); * * assertNotEquals(globalThis.setTimeout, setTimeout); * * // test timer related things. * * // You don't need to call fakeTime.restore() explicitly * // as it's implicitly called via the [Symbol.dispose] method * // when declared with `using`. * } * * assertEquals(globalThis.setTimeout, setTimeout); * ``` */ [Symbol.dispose]() { this.restore(); } /** * Restores real time. * * @throws {TimeError} If time is already restored * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assertEquals, assertNotEquals } from "@std/assert" * * const setTimeout = globalThis.setTimeout; * * const fakeTime = new FakeTime(); * * assertNotEquals(globalThis.setTimeout, setTimeout); * * FakeTime.restore(); * * assertEquals(globalThis.setTimeout, setTimeout); * ``` */ static restore() { if (!time) { throw new TimeError("Cannot restore time: time is already restored"); } time.restore(); } /** * Restores real time temporarily until callback returns and resolves. * * @throws {TimeError} If time is not faked * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assertEquals, assertNotEquals } from "@std/assert" * * const setTimeout = globalThis.setTimeout; * * const fakeTime = new FakeTime(); * * assertNotEquals(globalThis.setTimeout, setTimeout); * * FakeTime.restoreFor(() => { * assertEquals(globalThis.setTimeout, setTimeout); * }); * ``` * * @typeParam T The returned value type of the callback * @param callback The callback to be called while FakeTime being restored * @param args The arguments to pass to the callback * @returns The returned value from the callback */ static restoreFor<T>( // deno-lint-ignore no-explicit-any callback: (...args: any[]) => Promise<T> | T, // deno-lint-ignore no-explicit-any ...args: any[] ): Promise<T> { if (!time) { return Promise.reject( new TimeError("Cannot restore time: time is not faked"), ); } restoreGlobals(); try { const result = callback.apply(null, args); if (result instanceof Promise) { return result.finally(() => overrideGlobals()); } else { overrideGlobals(); return Promise.resolve(result); } } catch (e) { overrideGlobals(); return Promise.reject(e); } } /** * The number of milliseconds elapsed since the epoch (January 1, 1970 00:00:00 UTC) for the fake time. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assertEquals } from "@std/assert"; * * const fakeTime = new FakeTime(15_000); * * assertEquals(fakeTime.now, 15_000); * * fakeTime.tick(5_000); * * assertEquals(fakeTime.now, 20_000); * ``` * * @returns The number of milliseconds elapsed since the epoch (January 1, 1970 00:00:00 UTC) for the fake time */ get now(): number { return now; } /** * Set the current time. It will call any functions waiting to be called between the current and new fake time. * If the timer callback throws, time will stop advancing forward beyond that timer. * * @throws {RangeError} If the time goes backwards * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assertEquals } from "@std/assert"; * * const fakeTime = new FakeTime(15_000); * * assertEquals(fakeTime.now, 15_000); * * fakeTime.now = 35_000; * * assertEquals(fakeTime.now, 35_000); * ``` * * @param value The current time (in milliseconds) */ set now(value: number) { if (value < now) { throw new RangeError( `Cannot set current time in the past, time must be >= ${now}: received ${value}`, ); } let dueNode: DueNode | null = dueTree.min(); while (dueNode && dueNode.due <= value) { const timer: Timer | undefined = dueNode.timers.shift(); if (timer && dueNodes.has(timer.id)) { now = timer.due; if (timer.repeat) { const due: number = timer.due + timer.delay; let dueNode: DueNode | null = dueTree.find({ due } as DueNode); if (dueNode === null) { dueNode = { due, timers: [] }; dueTree.insert(dueNode); } dueNode.timers.push({ ...timer, due }); dueNodes.set(timer.id, dueNode); } else { dueNodes.delete(timer.id); } timer.callback.apply(null, timer.args); } else if (!timer) { dueTree.remove(dueNode); dueNode = dueTree.min(); } } now = value; } /** * The initial number of milliseconds elapsed since the epoch (January 1, 1970 00:00:00 UTC) for the fake time. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assertEquals } from "@std/assert"; * * const fakeTime = new FakeTime(15_000); * * assertEquals(fakeTime.start, 15_000); * ``` * * @returns The initial number of milliseconds elapsed since the epoch (January 1, 1970 00:00:00 UTC) for the fake time. */ get start(): number { return startedAt; } /** * Resolves after the given number of milliseconds using real time. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assertEquals } from "@std/assert"; * * const fakeTime = new FakeTime(15_000); * * await fakeTime.delay(500); // wait 500 ms in real time. * * assertEquals(fakeTime.now, 15_000); // The simulated time doesn't advance. * ``` * * @param ms The milliseconds to delay * @param options The options */ async delay(ms: number, options: DelayOptions = {}): Promise<void> { const { signal } = options; if (signal?.aborted) { return Promise.reject( new DOMException("Delay was aborted.", "AbortError"), ); } return await new Promise((resolve, reject) => { let timer: number | null = null; const abort = () => FakeTime .restoreFor(() => { if (timer) clearTimeout(timer); }) .then(() => reject(new DOMException("Delay was aborted.", "AbortError")) ); const done = () => { signal?.removeEventListener("abort", abort); resolve(); }; FakeTime.restoreFor(() => setTimeout(done, ms)) .then((id) => timer = id); signal?.addEventListener("abort", abort, { once: true }); }); } /** * Runs all pending microtasks. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assert } from "@std/assert"; * * const fakeTime = new FakeTime(15_000); * * let called = false; * * Promise.resolve().then(() => { called = true }); * * await fakeTime.runMicrotasks(); * * assert(called); * ``` */ async runMicrotasks() { await this.delay(0); } /** * Adds the specified number of milliseconds to the fake time. * This will call any functions waiting to be called between the current and new fake time. * * @example Usage * ```ts * import { * assertSpyCalls, * spy, * } from "@std/testing/mock"; * import { FakeTime } from "@std/testing/time"; * * function secondInterval(cb: () => void): number { * return setInterval(cb, 1000); * } * * Deno.test("secondInterval calls callback every second and stops after being cleared", () => { * using time = new FakeTime(); * * const cb = spy(); * const intervalId = secondInterval(cb); * assertSpyCalls(cb, 0); * time.tick(500); * assertSpyCalls(cb, 0); * time.tick(500); * assertSpyCalls(cb, 1); * time.tick(3500); * assertSpyCalls(cb, 4); * * clearInterval(intervalId); * time.tick(1000); * assertSpyCalls(cb, 4); * }); * ``` * * @param ms The milliseconds to advance */ tick(ms = 0) { this.now += ms; } /** * Runs all pending microtasks then adds the specified number of milliseconds to the fake time. * This will call any functions waiting to be called between the current and new fake time. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assert, assertEquals } from "@std/assert"; * * const fakeTime = new FakeTime(15_000); * * let called = false; * * Promise.resolve().then(() => { called = true }); * * await fakeTime.tickAsync(5_000); * * assert(called); * assertEquals(fakeTime.now, 20_000); * ``` * * @param ms The milliseconds to advance */ async tickAsync(ms = 0) { await this.runMicrotasks(); this.now += ms; } /** * Advances time to when the next scheduled timer is due. * If there are no pending timers, time will not be changed. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assert, assertEquals } from "@std/assert"; * * const fakeTime = new FakeTime(15_000); * * let called = false; * * setTimeout(() => { called = true }, 5000); * * fakeTime.next(); * * assert(called); * assertEquals(fakeTime.now, 20_000); * ``` * * @returns `true` when there is a scheduled timer and `false` when there is not. */ next(): boolean { const next = nextDueNode(); if (next) this.now = next.due; return !!next; } /** * Runs all pending microtasks then advances time to when the next scheduled timer is due. * If there are no pending timers, time will not be changed. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assert, assertEquals } from "@std/assert"; * * const fakeTime = new FakeTime(15_000); * * let called0 = false; * let called1 = false; * * setTimeout(() => { called0 = true }, 5000); * Promise.resolve().then(() => { called1 = true }); * * await fakeTime.nextAsync(); * * assert(called0); * assert(called1); * assertEquals(fakeTime.now, 20_000); * ``` * * @returns `true` if the pending timers existed and the time advanced, `false` if there was no pending timer and the time didn't advance. */ async nextAsync(): Promise<boolean> { await this.runMicrotasks(); return this.next(); } /** * Advances time forward to the next due timer until there are no pending timers remaining. * If the timers create additional timers, they will be run too. If there is an interval, * time will keep advancing forward until the interval is cleared. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assertEquals } from "@std/assert"; * * const fakeTime = new FakeTime(15_000); * * let count = 0; * * setTimeout(() => { count++ }, 5_000); * setTimeout(() => { count++ }, 15_000); * setTimeout(() => { count++ }, 35_000); * * fakeTime.runAll(); * * assertEquals(count, 3); * assertEquals(fakeTime.now, 50_000); * ``` */ runAll() { while (!dueTree.isEmpty()) { this.next(); } } /** * Advances time forward to the next due timer until there are no pending timers remaining. * If the timers create additional timers, they will be run too. If there is an interval, * time will keep advancing forward until the interval is cleared. * Runs all pending microtasks before each timer. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assertEquals } from "@std/assert"; * * const fakeTime = new FakeTime(15_000); * * let count = 0; * * setTimeout(() => { count++ }, 5_000); * setTimeout(() => { count++ }, 15_000); * setTimeout(() => { count++ }, 35_000); * Promise.resolve().then(() => { count++ }); * * await fakeTime.runAllAsync(); * * assertEquals(count, 4); * assertEquals(fakeTime.now, 50_000); * ``` */ async runAllAsync() { while (!dueTree.isEmpty()) { await this.nextAsync(); } } /** * Restores time related global functions to their original state. * * @example Usage * ```ts * import { FakeTime } from "@std/testing/time"; * import { assertEquals, assertNotEquals } from "@std/assert"; * * const setTimeout = globalThis.setTimeout; * * const fakeTime = new FakeTime(); // global timers are now faked * * assertNotEquals(globalThis.setTimeout, setTimeout); * * fakeTime.restore(); // timers are restored * * assertEquals(globalThis.setTimeout, setTimeout); * ``` */ restore() { if (!time) { throw new TimeError("Cannot restore time: time is already restored"); } time = undefined; restoreGlobals(); if (advanceIntervalId) clearInterval(advanceIntervalId); } }