Compared to Sinon
Each section below shows a test written with Sinon's fake timers on the left and the same test written with Time-Provider on the right. Every sample was run against Sinon 22.
Setup
Both sides set up their clock inside each test. Sinon patches the globals and has to put them back with clock.restore(), which a failed assertion skips, so the fake timers stay on for the tests that follow. A Time-Provider is a plain object: using disposes of it when the test ends, failed or not, along with any timer still pending.
Sinon
import sinon from "sinon";Time-Provider
import { createTimeProvider } from "@time-provider/core/deterministic";
import { plugin } from "@time-provider/plugin-native/deterministic";
import type { IUtcOnlyTimeProvider } from "@time-provider/core";using needs TypeScript 5.2 or later, or a Babel or SWC setup that supports explicit resource management.
A timeout fires after its delay
Time-Provider only controls the time it is given, so the code under test takes it as a parameter.
Code under test
Sinon
function scheduleReminder(send: () => void) {
setTimeout(send, 5 * 60_000);
}Time-Provider
function scheduleReminder(
timeProvider: IUtcOnlyTimeProvider<Date>,
send: () => void,
) {
timeProvider.scheduler.timers.once({ minutes: 5 }, send);
}Sinon
it("sends the reminder after 5 minutes", () => {
const clock = sinon.useFakeTimers();
const send = sinon.spy();
scheduleReminder(send);
clock.tick(4 * 60_000);
sinon.assert.notCalled(send);
clock.tick(60_000);
sinon.assert.calledOnce(send);
clock.restore();
});
// roughly measured to run in 3.2 ms (see below).Time-Provider
it("sends the reminder after 5 minutes", () => {
using timeProvider = createTimeProvider.for(plugin).asManual().create();
let sent = 0;
scheduleReminder(timeProvider, () => sent++);
timeProvider.clock.moveBy({ minutes: 4 });
expect(sent).toBe(0);
timeProvider.clock.moveBy({ minutes: 1 });
expect(sent).toBe(1);
});
// roughly measured to run in 1.0 ms (see below).An interval ticks
startPolling polls every 30 seconds.
Code under test
Sinon
function startPolling(poll: () => void) {
setInterval(poll, 30_000);
}Time-Provider
function startPolling(
timeProvider: IUtcOnlyTimeProvider<Date>,
poll: () => void,
) {
timeProvider.scheduler.timers.every({ seconds: 30 }, poll);
}Sinon
it("polls four times in two minutes", () => {
const clock = sinon.useFakeTimers();
const poll = sinon.spy();
startPolling(poll);
clock.tick(2 * 60_000);
sinon.assert.callCount(poll, 4);
clock.restore();
});
// roughly measured to run in 1.2 ms (see below).Time-Provider
it("polls four times in two minutes", () => {
using timeProvider = createTimeProvider.for(plugin).asManual().create();
let polls = 0;
startPolling(timeProvider, () => polls++);
timeProvider.clock.moveBy({ minutes: 2 });
expect(polls).toBe(4);
});
// roughly measured to run in 0.3 ms (see below).Reading the current time
createOrder stamps createdAt with the current time. A fixed clock never moves, so there is nothing to tick.
Code under test
Sinon
function createOrder() {
return { createdAt: new Date() };
}Time-Provider
function createOrder(
timeProvider: IUtcOnlyTimeProvider<Date>,
) {
return { createdAt: timeProvider.clock.utcNow() };
}Sinon
it("stamps the order", () => {
const now = new Date("2026-01-01");
const clock = sinon.useFakeTimers(now);
expect(createOrder().createdAt).toEqual(now);
clock.restore();
});
// roughly measured to run in 1.6 ms (see below).Time-Provider
it("stamps the order", () => {
const now = new Date("2026-01-01");
using timeProvider = createTimeProvider
.for(plugin)
.asFixed()
.withFixedTime(now)
.create();
expect(createOrder(timeProvider).createdAt).toEqual(now);
});
// roughly measured to run in 0.4 ms (see below).Running the next timer, then all of them
retry tries again after 1, 2 and 4 seconds, scheduling each attempt from the previous one.
Code under test
Sinon
function retry(attempt: () => void, delay = 1000) {
setTimeout(() => {
attempt();
if (delay < 4000) retry(attempt, delay * 2);
}, delay);
}Time-Provider
function retry(
timeProvider: IUtcOnlyTimeProvider<Date>,
attempt: () => void,
delay = 1000,
) {
const { timers } = timeProvider.scheduler;
timers.once({ milliseconds: delay }, () => {
attempt();
if (delay < 4000) retry(timeProvider, attempt, delay * 2);
});
}Sinon
it("retries three times", () => {
const clock = sinon.useFakeTimers();
const attempt = sinon.spy();
retry(attempt);
clock.next();
sinon.assert.calledOnce(attempt);
clock.runAll();
sinon.assert.calledThrice(attempt);
clock.restore();
});
// roughly measured to run in 1.0 ms (see below).Time-Provider
it("retries three times", () => {
using timeProvider = createTimeProvider.for(plugin).asManual().create();
let attempts = 0;
retry(timeProvider, () => attempts++);
timeProvider.clock.moveTo("nextTimer");
expect(attempts).toBe(1);
timeProvider.clock.moveUntil("noTimers");
expect(attempts).toBe(3);
});
// roughly measured to run in 0.4 ms (see below).clock.runToLast() is moveTo("lastTimer").
Async code
loadLater waits one second, then loads. Sinon needs tickAsync so the promise callbacks get to run between timers. Time-Provider fires the timer inside moveBy(), and the test awaits the result as usual.
Code under test
Sinon
async function loadLater(load: () => string) {
await new Promise((resolve) => setTimeout(resolve, 1000));
return load();
}Time-Provider
async function loadLater(
timeProvider: IUtcOnlyTimeProvider<Date>,
load: () => string,
) {
await timeProvider.scheduler.timers.wait({ seconds: 1 });
return load();
}Sinon
it("loads after a second", async () => {
const clock = sinon.useFakeTimers();
const result = loadLater(() => "loaded");
await clock.tickAsync(1000);
expect(await result).toBe("loaded");
clock.restore();
});
// roughly measured to run in 1.4 ms (see below).Time-Provider
it("loads after a second", async () => {
using timeProvider = createTimeProvider.for(plugin).asManual().create();
const result = loadLater(timeProvider, () => "loaded");
timeProvider.clock.moveBy({ seconds: 1 });
expect(await result).toBe("loaded");
});
// roughly measured to run in 0.3 ms (see below).Two reads that must differ
publish reads the time twice, for savedAt then publishedAt, with nothing in between for a test to hook into. Fake timers can't move time between those two reads. A sequential clock returns the next instant on each read.
Code under test
Sinon
function publish() {
const savedAt = Date.now();
return { savedAt, publishedAt: Date.now() };
}Time-Provider
function publish(
timeProvider: IUtcOnlyTimeProvider<Date>,
) {
const savedAt = timeProvider.clock.utcNow();
return { savedAt, publishedAt: timeProvider.clock.utcNow() };
}Sinon
Not supported.
Time-Provider
it("publishes after saving", () => {
using timeProvider = createTimeProvider
.for(plugin)
.asSequential()
.withSequentialTime(0)
.withSequentialTime(250)
.create();
expect(publish(timeProvider)).toEqual({
savedAt: new Date(0),
publishedAt: new Date(250),
});
});
// roughly measured to run in 0.5 ms (see below).Sleep
A check runs every minute, and the machine sleeps for eight hours. On wake-up the overdue check runs once, not 480 times.
Code under test
Sinon
function startChecking(check: () => void) {
setInterval(check, 60_000);
}Time-Provider
function startChecking(
timeProvider: IUtcOnlyTimeProvider<Date>,
check: () => void,
) {
timeProvider.scheduler.timers.every({ minutes: 1 }, check);
}Sinon
it("checks once after sleeping", () => {
const clock = sinon.useFakeTimers();
const check = sinon.spy();
startChecking(check);
clock.jump(8 * 3_600_000);
sinon.assert.calledOnce(check);
clock.restore();
});
// roughly measured to run in 1.0 ms (see below).Time-Provider
it("checks once after sleeping", () => {
using timeProvider = createTimeProvider.for(plugin).asManual().create();
let checks = 0;
startChecking(timeProvider, () => checks++);
timeProvider.clock.moveBy({ hours: 8 }, { as: "sleep" });
expect(checks).toBe(1);
});
// roughly measured to run in 0.3 ms (see below).clock.setSystemTime(t) changes the wall clock without firing anything. That is moveTo(t, { as: "snap" }), and a snap may go backward. See Moving the Clock.
Other code in the same test
Sinon replaces the timer globals, so every piece of code in the test runs on the fake clock, not only the code under test. Here the in-memory database answers through setTimeout, so saving never settles until something ticks the clock, and the test times out. A Time-Provider leaves the globals alone.
The in-memory database, the same on both sides
const user = { name: "Ada" };
const db = {
save<T>(record: T) {
return new Promise<T>((resolve) => {
setTimeout(() => resolve(record), 5);
});
},
};Sinon
it("saves the user", async () => {
const clock = sinon.useFakeTimers();
const saved = await db.save(user); // never settles
expect(saved).toEqual(user);
clock.restore();
});
// never finishes: the test times out.Time-Provider
it("saves the user", async () => {
using timeProvider = createTimeProvider.for(plugin).asManual().create();
const saved = await db.save(user);
expect(saved).toEqual(user);
});
// roughly measured to run in 5.6 ms (see below).Animation frames
spin redraws on every frame. Sinon fakes requestAnimationFrame only where the host has one, such as a browser, and fires frames on a 16ms grid, so a second holds 62 of them. The animation-frame addon runs at 60 frames per second by default, and withHostFramesRate() sets another rate.
Sinon
it("draws a second of frames", () => {
const clock = sinon.useFakeTimers();
let frames = 0;
const spin = () => {
frames++;
requestAnimationFrame(spin);
};
requestAnimationFrame(spin);
clock.tick(1000);
expect(frames).toBe(62);
clock.restore();
});
// roughly measured to run in 1.8 ms (see below).Time-Provider
import { addon } from "@time-provider/addon-animation-frame/deterministic";
it("draws a second of frames", () => {
using timeProvider = createTimeProvider.for(plugin).use(addon).asManual().create();
const { animation } = timeProvider.scheduler;
let frames = 0;
const spin = () => {
frames++;
animation.scheduleFrame(spin);
};
animation.scheduleFrame(spin);
timeProvider.clock.moveBy({ seconds: 1 });
expect(frames).toBe(60);
});
// roughly measured to run in 0.6 ms (see below).Idle callbacks
Sinon treats an idle callback as a timer, so the next tick runs every pending one. With the idle addon, the test decides when the host is idle and how many callbacks that idle period lets through.
Sinon
it("prefetches when idle", () => {
const clock = sinon.useFakeTimers();
const done: string[] = [];
for (const url of ["/a", "/b"]) {
requestIdleCallback(() => done.push(url));
}
clock.tick(0);
expect(done).toEqual(["/a", "/b"]);
clock.restore();
});
// roughly measured to run in 1.2 ms (see below).Time-Provider
import { addon } from "@time-provider/addon-idle/deterministic";
it("prefetches one page per idle period", () => {
using timeProvider = createTimeProvider.for(plugin).use(addon).asManual().create();
const { idle } = timeProvider.scheduler;
const done: string[] = [];
for (const url of ["/a", "/b"]) {
idle.request(() => done.push(url));
}
// Idle long enough for one callback; drain(n) allows n.
idle.drain(1);
expect(done).toEqual(["/a"]);
});
// roughly measured to run in 0.4 ms (see below).Microtasks
A microtask queued by the test itself runs on demand. One queued from a timer callback runs before the next timer, as it would on a real host.
Sinon
it("runs a queued microtask", () => {
const clock = sinon.useFakeTimers();
const log: string[] = [];
queueMicrotask(() => log.push("m"));
clock.runMicrotasks();
expect(log).toEqual(["m"]);
clock.restore();
});
// roughly measured to run in 0.7 ms (see below).
it("runs microtasks between timers", () => {
const clock = sinon.useFakeTimers();
const log: string[] = [];
setTimeout(() => {
log.push("t1");
queueMicrotask(() => log.push("m1"));
}, 10);
setTimeout(() => log.push("t2"), 10);
clock.tick(10);
expect(log).toEqual(["t1", "m1", "t2"]);
clock.restore();
});
// roughly measured to run in 0.7 ms (see below).Time-Provider
it("runs a queued microtask", () => {
using timeProvider = createTimeProvider.for(plugin).asManual().create();
const { microtasks } = timeProvider.scheduler;
const log: string[] = [];
microtasks.queue(() => log.push("m"));
microtasks.drain();
expect(log).toEqual(["m"]);
});
// roughly measured to run in 0.2 ms (see below).
it("runs microtasks between timers", () => {
using timeProvider = createTimeProvider.for(plugin).asManual().create();
const { timers, microtasks } = timeProvider.scheduler;
const log: string[] = [];
timers.once({ milliseconds: 10 }, () => {
log.push("t1");
microtasks.queue(() => log.push("m1"));
});
timers.once({ milliseconds: 10 }, () => log.push("t2"));
timeProvider.clock.moveBy({ milliseconds: 10 });
expect(log).toEqual(["t1", "m1", "t2"]);
});
// roughly measured to run in 0.3 ms (see below).See Microtasks.
How the timings were measured
Every sample on this page ran as a real test under Jest 30 on Node.js 22, with TypeScript compiled by SWC. The Sinon samples used Sinon 22. Sinon only replaces requestAnimationFrame and requestIdleCallback when the host has them, so its samples ran with no-op versions of both installed first. A wrapper around it read the real performance.now(), saved before any fake timers were installed, just before and just after the test body, so hooks and runner overhead are left out. Each file ran seven times and the figure is the median. The first test of each file includes warm-up. The numbers depend on the machine: compare the two columns with each other, not with your own runs.