Jest’s fake timers let you precisely control the passage of time in your tests, which is crucial for verifying code that relies on setTimeout, setInterval, and Date.
// src/timer-example.js
function delayedGreeting(name, callback) {
setTimeout(() => {
callback(`Hello, ${name}!`);
}, 1000);
}
function repeatingMessage(message, interval, callback) {
let count = 0;
const intervalId = setInterval(() => {
count++;
callback(message, count);
if (count >= 3) {
clearInterval(intervalId);
}
}, interval);
}
function timeTraveler(callback) {
const now = new Date();
callback(now.getHours());
}
module.exports = { delayedGreeting, repeatingMessage, timeTraveler };
// src/timer-example.test.js
const { delayedGreeting, repeatingMessage, timeTraveler } = require('./timer-example');
describe('Fake Timers', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
test('delayedGreeting should call callback after 1 second', () => {
const mockCallback = jest.fn();
delayedGreeting('World', mockCallback);
expect(mockCallback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000); // Advance time by 1000 milliseconds
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith('Hello, World!');
});
test('repeatingMessage should call callback multiple times', () => {
const mockCallback = jest.fn();
repeatingMessage('Tick', 500, mockCallback);
expect(mockCallback).not.toHaveBeenCalled();
jest.advanceTimersByTime(499); // Advance time just before the first interval
expect(mockCallback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1); // Exactly 500ms passed
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith('Tick', 1);
jest.advanceTimersByTime(500); // Another 500ms passed, total 1000ms
expect(mockCallback).toHaveBeenCalledTimes(2);
expect(mockCallback).toHaveBeenCalledWith('Tick', 2);
jest.advanceTimersByTime(500); // Another 500ms passed, total 1500ms
expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenCalledWith('Tick', 3);
jest.advanceTimersByTime(500); // Should not be called again as interval is cleared
expect(mockCallback).toHaveBeenCalledTimes(3);
});
test('timeTraveler should use the current date and time', () => {
const mockCallback = jest.fn();
const specificHour = 14; // 2 PM
// Mock the Date constructor to return a specific date/time
const mockDate = new Date(2023, 10, 20, specificHour, 30, 0); // Month is 0-indexed
global.Date = class extends Date {
constructor(dateString) {
if (dateString) {
super(dateString);
} else {
return mockDate;
}
}
};
timeTraveler(mockCallback);
expect(mockCallback).toHaveBeenCalledWith(specificHour);
// Restore original Date object
global.Date = Date;
});
test('should mock Date.now()', () => {
const mockCallback = jest.fn();
const specificTimestamp = 1678886400000; // March 15, 2023 12:00:00 AM GMT
jest.spyOn(Date, 'now').mockReturnValue(specificTimestamp);
// Assuming you have a function that uses Date.now()
const getTimestamp = () => Date.now();
mockCallback(getTimestamp());
expect(mockCallback).toHaveBeenCalledWith(specificTimestamp);
// Restore original Date.now()
jest.restoreAllMocks();
});
test('should advance timers to the next pending timer', () => {
const mockCallback1 = jest.fn();
const mockCallback2 = jest.fn();
delayedGreeting('First', mockCallback1); // Schedule for 1000ms
delayedGreeting('Second', mockCallback2); // Schedule for 1000ms
jest.advanceTimersByTime(500);
expect(mockCallback1).not.toHaveBeenCalled();
expect(mockCallback2).not.toHaveBeenCalled();
jest.advanceTimersByTime(500); // Advance to exactly 1000ms, both should fire
expect(mockCallback1).toHaveBeenCalledTimes(1);
expect(mockCallback2).toHaveBeenCalledTimes(1);
});
test('should clear intervals', () => {
const mockCallback = jest.fn();
const intervalId = setInterval(() => {
mockCallback('This should not be called');
}, 1000);
jest.advanceTimersByTime(500);
expect(mockCallback).not.toHaveBeenCalled();
clearInterval(intervalId);
jest.advanceTimersByTime(1000); // Even after this time, it shouldn't be called
expect(mockCallback).not.toHaveBeenCalled();
});
});
The core idea is to swap out the real, asynchronous setTimeout, setInterval, and Date implementations with Jest’s controlled, synchronous versions. This allows your tests to execute timer callbacks immediately or at specific simulated points in time, making your tests deterministic and fast.
When jest.useFakeTimers() is called, Jest replaces the global setTimeout, setInterval, clearTimeout, clearInterval, and Date objects with its own mock implementations. These mocks don’t actually wait for time to pass. Instead, they queue up the callbacks and wait for you to tell Jest to advance the simulated clock.
The most common way to advance the clock is jest.advanceTimersByTime(ms). This method tells Jest to execute all timer callbacks that are scheduled to run within the specified ms. If you have a setTimeout scheduled for 1000ms, calling jest.advanceTimersByTime(1000) will execute that callback immediately within your test. If you call it with 500, the callback will remain queued, waiting for more time to pass.
For Date manipulation, you have a couple of options. You can mock Date.now() directly using jest.spyOn(Date, 'now').mockReturnValue(timestamp). This is useful if your code specifically calls Date.now(). If your code instantiates new Date() and expects it to represent a certain time, you can override the global Date constructor as shown in the timeTraveler test. This involves creating a new class that extends Date and controls its constructor’s behavior, returning your mocked date when new Date() is called without arguments. Remember to restore the original Date object after your test using global.Date = Date; or jest.restoreAllMocks().
The jest.advanceTimersToNextTimer() function is a handy shortcut. Instead of specifying an exact millisecond amount, it advances the timers just enough to execute the next scheduled timer callback. This is particularly useful when you have multiple timers with different durations and you want to ensure each one runs in sequence without manually calculating the time differences.
When dealing with setInterval, Jest’s fake timers will continue to execute the callback on each interval as you advance time. The crucial part here is often ensuring that clearInterval is correctly called. Your test should verify that after clearInterval is invoked, subsequent calls to jest.advanceTimersByTime do not trigger the interval callback anymore.
A common pitfall is forgetting to call jest.useRealTimers() in an afterAll or afterEach hook. If you leave fake timers active, they can interfere with other tests or even parts of Jest itself that might rely on real-time behavior. The jest.useFakeTimers() and jest.useRealTimers() calls create a sandbox for your timer manipulation.
The most powerful, yet often overlooked, aspect of Jest’s fake timers is their ability to mock system clocks for asynchronous operations beyond just setTimeout and setInterval. For instance, libraries that use requestAnimationFrame or even network requests that have timeouts can be influenced by fake timers if they internally use or are compatible with the timer APIs. You can also use jest.advanceTimersByTime(Infinity) to run all pending timers to completion.
The next step after mastering fake timers is often understanding how to mock asynchronous operations like network requests using Jest’s jest.mock and jest.fn() in conjunction with Promise.resolve() or async/await patterns.