feat(testing): add utility for faking time (#2069)

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
Kyle June 2022-04-14 01:29:17 -05:00 committed by GitHub
parent 5d7915ccec
commit 403f226a20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1121 additions and 0 deletions

View File

@ -796,3 +796,56 @@ Deno.test("randomMultiple uses randomInt to generate random multiples between -1
assertSpyCalls(randomIntStub, 2);
});
```
### Faking time
Say we have a function that has time based behavior that we would like to test.
With real time, that could cause tests to take much longer than they should. If
you fake time, you could simulate how your function would behave over time
starting from any point in time. Below is an example where we want to test that
the callback is called every second.
```ts
// https://deno.land/std@$STD_VERSION/testing/mock_examples/interval.ts
export function secondInterval(cb: () => void): number {
return setInterval(cb, 1000);
}
```
With `FakeTime` we can do that. When the `FakeTime` instance is created, it
splits from real time. The `Date`, `setTimeout`, `clearTimeout`, `setInterval`
and `clearInterval` globals are replaced with versions that use the fake time
until real time is restored. You can control how time ticks forward with the
`tick` method on the `FakeTime` instance.
```ts
// https://deno.land/std@$STD_VERSION/testing/mock_examples/interval_test.ts
import {
assertSpyCalls,
spy,
} from "https://deno.land/std@$STD_VERSION/testing/mock.ts";
import { FakeTime } from "https://deno.land/std@$STD_VERSION/testing/time.ts";
import { secondInterval } from "https://deno.land/std@$STD_VERSION/testing/mock_examples/interval.ts";
Deno.test("secondInterval calls callback every second and stops after being cleared", () => {
const time = new FakeTime();
try {
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);
} finally {
time.restore();
}
});
```

10
testing/_time.ts Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
/** Used internally for testing that fake time uses real time correctly. */
export const _internals = {
Date,
setTimeout,
clearTimeout,
setInterval,
clearInterval,
};

View File

@ -0,0 +1,4 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
export function secondInterval(cb: () => void): number {
return setInterval(cb, 1000);
}

View File

@ -0,0 +1,26 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import { assertSpyCalls, spy } from "../mock.ts";
import { FakeTime } from "../time.ts";
import { secondInterval } from "./interval.ts";
Deno.test("secondInterval calls callback every second and stops after being cleared", () => {
const time = new FakeTime();
const cb = spy();
try {
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);
} finally {
time.restore();
}
});

460
testing/time.ts Normal file
View File

@ -0,0 +1,460 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
/** This module is browser compatible. */
import { ascend, RBTree } from "../collections/rb_tree.ts";
import { DelayOptions } from "../async/delay.ts";
import { _internals } from "./_time.ts";
/** An error related to faking time. */
export class TimeError extends Error {
constructor(message: string) {
super(message);
this.name = "TimeError";
}
}
function isFakeDate(instance: unknown): instance is FakeDate {
return instance instanceof FakeDate;
}
interface FakeDate extends Date {
date: Date;
}
function FakeDate(this: void): string;
function FakeDate(this: FakeDate): void;
function FakeDate(
this: FakeDate,
value: string | number | Date,
): void;
function FakeDate(
this: FakeDate,
year: number,
month: number,
date?: number,
hours?: number,
minutes?: number,
seconds?: number,
ms?: number,
): void;
function FakeDate(
this: FakeDate | void,
// deno-lint-ignore no-explicit-any
...args: any[]
): string | void {
if (args.length === 0) args.push(FakeDate.now());
if (isFakeDate(this)) {
this.date = new _internals.Date(...(args as []));
} else {
return new _internals.Date(args[0]).toString();
}
}
FakeDate.parse = Date.parse;
FakeDate.UTC = Date.UTC;
FakeDate.now = () => time?.now ?? _internals.Date.now();
Object.getOwnPropertyNames(Date.prototype).forEach((name: string) => {
const propName: keyof Date = name as keyof Date;
FakeDate.prototype[propName] = function (
this: FakeDate,
// deno-lint-ignore no-explicit-any
...args: any[]
// deno-lint-ignore no-explicit-any
): any {
// deno-lint-ignore no-explicit-any
return (this.date[propName] as (...args: any[]) => any).apply(
this.date,
args,
);
};
});
Object.getOwnPropertySymbols(Date.prototype).forEach((name: symbol) => {
const propName: keyof Date = name as unknown as keyof Date;
FakeDate.prototype[propName] = function (
this: FakeDate,
// deno-lint-ignore no-explicit-any
...args: any[]
// deno-lint-ignore no-explicit-any
): any {
// deno-lint-ignore no-explicit-any
return (this.date[propName] as (...args: any[]) => any).apply(
this.date,
args,
);
};
});
interface Timer {
id: number;
// deno-lint-ignore no-explicit-any
callback: (...args: any[]) => void;
delay: number;
args: unknown[];
due: number;
repeat: boolean;
}
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 foward at the same rate in milliseconds as real time.
*/
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.
*/
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("no fake time");
return setTimer(callback, delay, args, false);
}
function fakeClearTimeout(id?: number): void {
if (!time) throw new TimeError("no fake time");
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("no fake time");
return setTimer(callback, delay, args, true);
}
function fakeClearInterval(id?: number): void {
if (!time) throw new TimeError("no fake time");
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 overrideGlobals(): void {
globalThis.Date = FakeDate as DateConstructor;
globalThis.setTimeout = fakeSetTimeout;
globalThis.clearTimeout = fakeClearTimeout;
globalThis.setInterval = fakeSetInterval;
globalThis.clearInterval = fakeClearInterval;
}
function restoreGlobals(): void {
globalThis.Date = _internals.Date;
globalThis.setTimeout = _internals.setTimeout;
globalThis.clearTimeout = _internals.clearTimeout;
globalThis.setInterval = _internals.setInterval;
globalThis.clearInterval = _internals.clearInterval;
}
function* timerIdGen() {
let i = 1;
while (true) yield i++;
}
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: RBTree<DueNode>;
/**
* Overrides the real Date object and timer functions with fake ones that can be
* controlled through the fake time instance.
*
* ```ts
* // https://deno.land/std@$STD_VERSION/testing/mock_examples/interval_test.ts
* import {
* assertSpyCalls,
* spy,
* } from "https://deno.land/std@$STD_VERSION/testing/mock.ts";
* import { FakeTime } from "https://deno.land/std@$STD_VERSION/testing/time.ts";
* import { secondInterval } from "https://deno.land/std@$STD_VERSION/testing/mock_examples/interval.ts";
*
* Deno.test("secondInterval calls callback every second and stops after being cleared", () => {
* const time = new FakeTime();
*
* try {
* 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);
* } finally {
* time.restore();
* }
* });
* ```
*/
export class FakeTime {
constructor(
start?: number | string | Date | null,
options?: FakeTimeOptions,
) {
if (time) time.restore();
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 TimeError("invalid start");
now = startedAt;
timerId = timerIdGen();
dueNodes = new Map();
dueTree = new RBTree(
(a: DueNode, b: DueNode) => ascend(a.due, b.due),
);
overrideGlobals();
time = this;
advanceRate = Math.max(
0,
options?.advanceRate ? options.advanceRate : 0,
);
advanceFrequency = Math.max(
0,
options?.advanceFrequency ? options.advanceFrequency : 10,
);
advanceIntervalId = advanceRate > 0
? _internals.setInterval.call(null, () => {
this.tick(advanceRate * advanceFrequency);
}, advanceFrequency)
: undefined;
}
/** Restores real time. */
static restore(): void {
if (!time) throw new TimeError("time already restored");
time.restore();
}
/**
* Restores real time temporarily until callback returns and resolves.
*/
static async 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) throw new TimeError("no fake time");
let result: T;
restoreGlobals();
try {
result = await callback.apply(null, args);
} finally {
overrideGlobals();
}
return result;
}
/**
* The amount of milliseconds elapsed since January 1, 1970 00:00:00 UTC for the fake time.
* When set, 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.
*/
get now(): number {
return now;
}
set now(value: number) {
if (value < now) throw new Error("time cannot go backwards");
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 amount of milliseconds elapsed since January 1, 1970 00:00:00 UTC for the fake time. */
get start(): number {
return startedAt;
}
set start(value: number) {
throw new Error("cannot change start time after initialization");
}
/** Resolves after the given number of milliseconds using real time. */
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. */
async runMicrotasks(): Promise<void> {
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.
*/
tick(ms = 0): void {
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.
*/
async tickAsync(ms = 0): Promise<void> {
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.
* Returns true when there is a scheduled timer and false when there is not.
*/
next(): boolean {
const next = dueTree.min();
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.
*/
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.
*/
runAll(): void {
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.
*/
async runAllAsync(): Promise<void> {
while (!dueTree.isEmpty()) {
await this.nextAsync();
}
}
/** Restores time related global functions to their original state. */
restore(): void {
if (!time) throw new TimeError("time already restored");
time = undefined;
restoreGlobals();
if (advanceIntervalId) clearInterval(advanceIntervalId);
}
}

568
testing/time_test.ts Normal file
View File

@ -0,0 +1,568 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import {
assert,
assertEquals,
assertInstanceOf,
assertNotEquals,
assertRejects,
assertStrictEquals,
} from "./asserts.ts";
import { FakeTime } from "./time.ts";
import { _internals } from "./_time.ts";
import { assertSpyCall, MockError, spy, SpyCall, stub } from "./mock.ts";
function fromNow(): () => number {
const start: number = Date.now();
return () => Date.now() - start;
}
Deno.test("Date unchanged if FakeTime is uninitialized", () => {
assertStrictEquals(Date, _internals.Date);
});
Deno.test("Date is fake if FakeTime is initialized", () => {
const time = new FakeTime(9001);
try {
assertNotEquals(Date, _internals.Date);
} finally {
time.restore();
}
assertStrictEquals(Date, _internals.Date);
});
Deno.test("Fake Date parse and UTC behave the same", () => {
const expectedUTC = Date.UTC(96, 1, 2, 3, 4, 5);
const expectedParse = Date.parse("04 Dec 1995 00:12:00 GMT");
const time = new FakeTime();
try {
assertEquals(
Date.UTC(96, 1, 2, 3, 4, 5),
expectedUTC,
);
assertEquals(
Date.parse("04 Dec 1995 00:12:00 GMT"),
expectedParse,
);
} finally {
time.restore();
}
});
Deno.test("Fake Date.now returns current fake time", () => {
const time: FakeTime = new FakeTime(9001);
const now = spy(_internals.Date, "now");
try {
assertEquals(Date.now(), 9001);
assertEquals(now.calls.length, 0);
time.tick(1523);
assertEquals(Date.now(), 10524);
assertEquals(now.calls.length, 0);
} finally {
time.restore();
now.restore();
}
});
Deno.test("Fake Date instance methods passthrough to real Date instance methods", () => {
const time = new FakeTime();
try {
const now = new Date("2020-05-25T05:00:00.12345Z");
assertEquals(now.toISOString(), "2020-05-25T05:00:00.123Z");
Object.getOwnPropertyNames(_internals.Date.prototype).forEach(
(method: string) => {
if (
typeof _internals.Date.prototype[method as keyof Date] === "function"
) {
if (typeof now[method as keyof Date] !== "function") {
throw new MockError(`FakeDate missing ${method} method`);
}
const returned = Symbol();
const func = stub(
_internals.Date.prototype,
method as keyof Date,
() => returned,
);
try {
const args = Array(5).fill(undefined).map(() => Symbol());
(now[method as keyof Date] as CallableFunction)(...args);
assertSpyCall(func, 0, {
args,
returned,
});
assertInstanceOf(func.calls[0].self, _internals.Date);
} finally {
func.restore();
}
}
},
);
Object.getOwnPropertySymbols(_internals.Date.prototype).forEach(
(method: symbol) => {
if (
typeof _internals.Date.prototype[method as keyof Date] === "function"
) {
if (typeof now[method as keyof Date] !== "function") {
throw new MockError(`FakeDate missing ${method.toString()} method`);
}
const returned = Symbol();
const func = stub(
_internals.Date.prototype,
method as keyof Date,
() => returned,
);
try {
const args = Array(5).fill(undefined).map(() => Symbol());
(now[method as keyof Date] as CallableFunction)(...args);
assertSpyCall(func, 0, {
args,
returned,
});
assertInstanceOf(func.calls[0].self, _internals.Date);
} finally {
func.restore();
}
}
},
);
} finally {
time.restore();
}
});
Deno.test("timeout functions unchanged if FakeTime is uninitialized", () => {
assertStrictEquals(setTimeout, _internals.setTimeout);
assertStrictEquals(clearTimeout, _internals.clearTimeout);
});
Deno.test("timeout functions are fake if FakeTime is initialized", () => {
const time: FakeTime = new FakeTime();
try {
assertNotEquals(setTimeout, _internals.setTimeout);
assertNotEquals(clearTimeout, _internals.clearTimeout);
} finally {
time.restore();
}
assertStrictEquals(setTimeout, _internals.setTimeout);
assertStrictEquals(clearTimeout, _internals.clearTimeout);
});
Deno.test("FakeTime only ticks forward when setting now or calling tick", () => {
const time: FakeTime = new FakeTime();
const start: number = Date.now();
try {
assertEquals(Date.now(), start);
time.tick(5);
assertEquals(Date.now(), start + 5);
time.now = start + 1000;
assertEquals(Date.now(), start + 1000);
assert(_internals.Date.now() < start + 1000);
} finally {
time.restore();
}
});
Deno.test("FakeTime controls timeouts", () => {
const time: FakeTime = new FakeTime();
const start: number = Date.now();
const cb = spy(fromNow());
const expected: SpyCall[] = [];
try {
setTimeout(cb, 1000);
time.tick(250);
assertEquals(cb.calls, expected);
time.tick(250);
assertEquals(cb.calls, expected);
time.tick(500);
expected.push({ args: [], returned: 1000 });
assertEquals(cb.calls, expected);
time.tick(2500);
assertEquals(cb.calls, expected);
setTimeout(cb, 1000, "a");
setTimeout(cb, 2000, "b");
setTimeout(cb, 1500, "c");
assertEquals(cb.calls, expected);
time.tick(2500);
expected.push({ args: ["a"], returned: 4500 });
expected.push({ args: ["c"], returned: 5000 });
expected.push({ args: ["b"], returned: 5500 });
assertEquals(cb.calls, expected);
setTimeout(cb, 1000, "a");
setTimeout(cb, 1500, "b");
const timeout: number = setTimeout(cb, 1750, "c");
setTimeout(cb, 2000, "d");
time.tick(1250);
expected.push({ args: ["a"], returned: 7000 });
assertEquals(cb.calls, expected);
assertEquals(Date.now(), start + 7250);
clearTimeout(timeout);
time.tick(500);
expected.push({ args: ["b"], returned: 7500 });
assertEquals(cb.calls, expected);
assertEquals(Date.now(), start + 7750);
time.tick(250);
expected.push({ args: ["d"], returned: 8000 });
assertEquals(cb.calls, expected);
} finally {
time.restore();
}
});
Deno.test("interval functions unchanged if FakeTime is uninitialized", () => {
assertStrictEquals(setInterval, _internals.setInterval);
assertStrictEquals(clearInterval, _internals.clearInterval);
});
Deno.test("interval functions are fake if FakeTime is initialized", () => {
const time: FakeTime = new FakeTime();
try {
assertNotEquals(setInterval, _internals.setInterval);
assertNotEquals(clearInterval, _internals.clearInterval);
} finally {
time.restore();
}
assertStrictEquals(setInterval, _internals.setInterval);
assertStrictEquals(clearInterval, _internals.clearInterval);
});
Deno.test("FakeTime controls intervals", () => {
const time: FakeTime = new FakeTime();
const cb = spy(fromNow());
const expected: SpyCall[] = [];
try {
const interval: number = setInterval(cb, 1000);
time.tick(250);
assertEquals(cb.calls, expected);
time.tick(250);
assertEquals(cb.calls, expected);
time.tick(500);
expected.push({ args: [], returned: 1000 });
assertEquals(cb.calls, expected);
time.tick(2500);
expected.push({ args: [], returned: 2000 });
expected.push({ args: [], returned: 3000 });
assertEquals(cb.calls, expected);
clearInterval(interval);
time.tick(1000);
assertEquals(cb.calls, expected);
} finally {
time.restore();
}
});
Deno.test("FakeTime calls timeout and interval callbacks in correct order", () => {
const time: FakeTime = new FakeTime();
const cb = spy(fromNow());
const timeoutCb = spy(cb);
const intervalCb = spy(cb);
const expected: SpyCall[] = [];
const timeoutExpected: SpyCall[] = [];
const intervalExpected: SpyCall[] = [];
try {
const interval: number = setInterval(intervalCb, 1000);
setTimeout(timeoutCb, 500);
time.tick(250);
assertEquals(intervalCb.calls, intervalExpected);
time.tick(250);
setTimeout(timeoutCb, 1000);
let expect: SpyCall = { args: [], returned: 500 };
expected.push(expect);
timeoutExpected.push(expect);
assertEquals(cb.calls, expected);
assertEquals(timeoutCb.calls, timeoutExpected);
assertEquals(cb.calls, expected);
assertEquals(intervalCb.calls, intervalExpected);
time.tick(500);
expect = { args: [], returned: 1000 };
expected.push(expect);
intervalExpected.push(expect);
assertEquals(cb.calls, expected);
assertEquals(intervalCb.calls, intervalExpected);
time.tick(2500);
expect = { args: [], returned: 1500 };
expected.push(expect);
timeoutExpected.push(expect);
expect = { args: [], returned: 2000 };
expected.push(expect);
intervalExpected.push(expect);
expect = { args: [], returned: 3000 };
expected.push(expect);
intervalExpected.push(expect);
assertEquals(cb.calls, expected);
assertEquals(timeoutCb.calls, timeoutExpected);
assertEquals(intervalCb.calls, intervalExpected);
clearInterval(interval);
time.tick(1000);
assertEquals(cb.calls, expected);
assertEquals(timeoutCb.calls, timeoutExpected);
assertEquals(intervalCb.calls, intervalExpected);
} finally {
time.restore();
}
});
Deno.test("FakeTime restoreFor restores real time temporarily", async () => {
const time: FakeTime = new FakeTime();
const start: number = Date.now();
try {
assertEquals(Date.now(), start);
time.tick(1000);
assertEquals(Date.now(), start + 1000);
assert(_internals.Date.now() < start + 1000);
await FakeTime.restoreFor(() => {
assert(Date.now() < start + 1000);
});
assertEquals(Date.now(), start + 1000);
assert(_internals.Date.now() < start + 1000);
} finally {
time.restore();
}
});
Deno.test("delay uses real time", async () => {
const time: FakeTime = new FakeTime();
const start: number = Date.now();
try {
assertEquals(Date.now(), start);
await time.delay(20);
assert(_internals.Date.now() >= start + 20);
assertEquals(Date.now(), start);
} finally {
time.restore();
}
});
Deno.test("delay runs all microtasks before resolving", async () => {
const time: FakeTime = new FakeTime();
try {
const seq = [];
queueMicrotask(() => seq.push(2));
queueMicrotask(() => seq.push(3));
seq.push(1);
await time.delay(20);
seq.push(4);
assertEquals(seq, [1, 2, 3, 4]);
} finally {
time.restore();
}
});
Deno.test("delay with abort", async () => {
const time: FakeTime = new FakeTime();
try {
const seq = [];
const abort = new AbortController();
const { signal } = abort;
const delayedPromise = time.delay(100, { signal });
seq.push(1);
await FakeTime.restoreFor(() => {
setTimeout(() => {
seq.push(2);
abort.abort();
}, 0);
});
await assertRejects(
() => delayedPromise,
DOMException,
"Delay was aborted",
);
seq.push(3);
assertEquals(seq, [1, 2, 3]);
} finally {
time.restore();
}
});
Deno.test("runMicrotasks runs all microtasks before resolving", async () => {
const time: FakeTime = new FakeTime();
const start: number = Date.now();
try {
const seq = [];
queueMicrotask(() => seq.push(2));
queueMicrotask(() => seq.push(3));
seq.push(1);
await time.runMicrotasks();
seq.push(4);
assertEquals(seq, [1, 2, 3, 4]);
assertEquals(Date.now(), start);
} finally {
time.restore();
}
});
Deno.test("tickAsync runs all microtasks and runs timers if ticks past due", async () => {
const time: FakeTime = new FakeTime();
const start: number = Date.now();
const cb = spy(fromNow());
const expected: SpyCall[] = [];
const seq: number[] = [];
try {
setTimeout(cb, 1000);
queueMicrotask(() => seq.push(2));
queueMicrotask(() => seq.push(3));
seq.push(1);
await time.tickAsync(250);
seq.push(4);
assertEquals(cb.calls, expected);
await time.tickAsync(250);
assertEquals(cb.calls, expected);
queueMicrotask(() => seq.push(6));
seq.push(5);
await time.tickAsync(500);
seq.push(7);
expected.push({ args: [], returned: 1000 });
assertEquals(cb.calls, expected);
assertEquals(Date.now(), start + 1000);
assertEquals(seq, [1, 2, 3, 4, 5, 6, 7]);
} finally {
time.restore();
}
});
Deno.test("runNext runs next timer without running microtasks", async () => {
const time: FakeTime = new FakeTime();
const start: number = Date.now();
const cb = spy(fromNow());
const seq: number[] = [];
try {
setTimeout(cb, 1000);
queueMicrotask(() => seq.push(3));
queueMicrotask(() => seq.push(4));
seq.push(1);
time.next();
seq.push(2);
const expectedCalls = [{ args: [], returned: 1000 }];
assertEquals(cb.calls, expectedCalls);
assertEquals(Date.now(), start + 1000);
await time.runMicrotasks();
queueMicrotask(() => seq.push(7));
queueMicrotask(() => seq.push(8));
seq.push(5);
time.next();
seq.push(6);
await time.runMicrotasks();
assertEquals(cb.calls, expectedCalls);
assertEquals(Date.now(), start + 1000);
assertEquals(seq, [1, 2, 3, 4, 5, 6, 7, 8]);
} finally {
time.restore();
}
});
Deno.test("runNextAsync runs all microtasks and next timer", async () => {
const time: FakeTime = new FakeTime();
const start: number = Date.now();
const cb = spy(fromNow());
const seq: number[] = [];
try {
setTimeout(cb, 1000);
queueMicrotask(() => seq.push(2));
queueMicrotask(() => seq.push(3));
seq.push(1);
await time.nextAsync();
seq.push(4);
const expectedCalls = [{ args: [], returned: 1000 }];
assertEquals(cb.calls, expectedCalls);
assertEquals(Date.now(), start + 1000);
queueMicrotask(() => seq.push(6));
queueMicrotask(() => seq.push(7));
seq.push(5);
await time.nextAsync();
seq.push(8);
assertEquals(cb.calls, expectedCalls);
assertEquals(Date.now(), start + 1000);
assertEquals(seq, [1, 2, 3, 4, 5, 6, 7, 8]);
} finally {
time.restore();
}
});
Deno.test("runAll runs all timers without running microtasks", async () => {
const time: FakeTime = new FakeTime();
const start: number = Date.now();
const cb = spy(fromNow());
const seq: number[] = [];
try {
setTimeout(cb, 1000);
setTimeout(cb, 1500);
queueMicrotask(() => seq.push(3));
queueMicrotask(() => seq.push(4));
seq.push(1);
time.runAll();
seq.push(2);
const expectedCalls = [
{ args: [], returned: 1000 },
{ args: [], returned: 1500 },
];
assertEquals(cb.calls, expectedCalls);
assertEquals(Date.now(), start + 1500);
await time.runMicrotasks();
queueMicrotask(() => seq.push(7));
queueMicrotask(() => seq.push(8));
seq.push(5);
time.runAll();
seq.push(6);
await time.runMicrotasks();
assertEquals(cb.calls, expectedCalls);
assertEquals(Date.now(), start + 1500);
assertEquals(seq, [1, 2, 3, 4, 5, 6, 7, 8]);
} finally {
time.restore();
}
});
Deno.test("runAllAsync runs all microtasks and timers", async () => {
const time: FakeTime = new FakeTime();
const start: number = Date.now();
const cb = spy(fromNow());
const seq: number[] = [];
try {
setTimeout(cb, 1000);
setTimeout(cb, 1500);
queueMicrotask(() => seq.push(2));
queueMicrotask(() => seq.push(3));
seq.push(1);
await time.runAllAsync();
seq.push(4);
const expectedCalls = [
{ args: [], returned: 1000 },
{ args: [], returned: 1500 },
];
assertEquals(cb.calls, expectedCalls);
assertEquals(Date.now(), start + 1500);
queueMicrotask(() => seq.push(6));
queueMicrotask(() => seq.push(7));
seq.push(5);
await time.runAllAsync();
seq.push(8);
assertEquals(cb.calls, expectedCalls);
assertEquals(Date.now(), start + 1500);
assertEquals(seq, [1, 2, 3, 4, 5, 6, 7, 8]);
} finally {
time.restore();
}
});