Mock Time with Sinon Fake Timers
Control time in tests with fake timers so debounce, retry, and timeout logic is deterministic instead of flaky.
Task
Use fake timers to advance time deterministically and test debounce and retry behaviour.
When to use this
A debounce test is flaky because it depends on real timers and race conditions with CI load.
Prerequisites
- A test runner with sinon or an equivalent fake-timer API
- Code using timers
- A debounced function under test
Steps
1. Install fake timers before the test and advance them explicitly.
const clock = sinon.useFakeTimers();
let calls = 0;
const debounced = debounce(() => calls++, 300);
debounced(); debounced(); clock.tick(299);
expect(calls).toBe(0);
clock.tick(1);
expect(calls).toBe(1);
clock.restore();Expected output: The debounce fires exactly once after the window, with no real waiting.
2. Restore timers after each test.
afterEach(() => sinon.restore());Expected output: Fake timers cannot leak into other tests and cause confusing failures.
Troubleshooting
Problem
Promises never resolve under fake timers
Solution
Flush microtasks with await Promise.resolve() between ticks, or use a timer API that supports async.
Problem
Tests hang
Solution
Always restore timers in afterEach so real timers return.
Limitations
- Fake timers do not cover real I/O timing.
- Microtask ordering needs care with promises.
- Over-mocking time can hide real delays.
What this recipe does
Use fake timers to advance time deterministically and test debounce and retry behaviour.
A debounce test is flaky because it depends on real timers and race conditions with CI load.
Prerequisites
- A test runner with sinon or an equivalent fake-timer API
- Code using timers
- A debounced function under test
Procedure
- Install fake timers before the test and advance them explicitly.
const clock = sinon.useFakeTimers();
let calls = 0;
const debounced = debounce(() => calls++, 300);
debounced(); debounced(); clock.tick(299);
expect(calls).toBe(0);
clock.tick(1);
expect(calls).toBe(1);
clock.restore();
Expected result: The debounce fires exactly once after the window, with no real waiting.
- Restore timers after each test.
afterEach(() => sinon.restore());
Expected result: Fake timers cannot leak into other tests and cause confusing failures.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows the debounce test passes deterministically across many runs and fails when the window constant changes unexpectedly.
Troubleshooting
Promises never resolve under fake timers — Flush microtasks with await Promise.resolve() between ticks, or use a timer API that supports async.
Tests hang — Always restore timers in afterEach so real timers return.
Limitations
- Fake timers do not cover real I/O timing.
- Microtask ordering needs care with promises.
- Over-mocking time can hide real delays.