Моделирование таймеров
Встроенные функции таймеров (т.е., setTimeout(), setInterval(), clearTimeout(), clearInterval()) не являются оптимальными для тестовой среды, так как они зависят от реального хода времени. Jest может заменить таймеры функциями, позволяющими контролировать течение времени. Великолепно!
Также см. документацию по API фейковых таймеров.
Включение фейковых таймеров
В следующем примере мы включаем фейковые таймеры, вызвав jest.useFakeTimers(). Это заменяет исходную реализацию setTimeout() и других функций таймеров. Таймеры могут быть восстановлены до их обычного поведения с помощью jest.useRealTimers().
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
module.exports = timerGame;
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');
test('waits 1 second before ending the game', () => {
const timerGame = require('../timerGame');
timerGame();
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});
Запуск всех таймеров
Другой тест, который мы можем написать для этого модуля, — это тест, проверяющий, что обратный вызов выполняется через 1 секунду. Для этого мы будем использовать API управления таймерами Jest, чтобы ускорить время прямо в середине теста:
jest.useFakeTimers();
test('calls the callback after 1 second', () => {
const timerGame = require('../timerGame');
const callback = jest.fn();
timerGame(callback);
// At this point in time, the callback should not have been called yet
expect(callback).not.toBeCalled();
// Fast-forward until all timers have been executed
jest.runAllTimers();
// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
Запуск ожидающих таймеров
Также могут быть сценарии, в которых у вас есть рекурсивный таймер — это таймер, который устанавливает новый таймер в своём собственном обратном вызове. В этих случаях запуск всех таймеров будет бесконечным циклом, выбросив следующую ошибку: "Прерывание после запуска 100000 таймеров, предполагая бесконечный цикл!"
В таком случае использование jest.runOnlyPendingTimers() решит проблему:
function infiniteTimerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up! 10 seconds before the next game starts...");
callback && callback();
// Schedule the next game in 10 seconds
setTimeout(() => {
infiniteTimerGame(callback);
}, 10000);
}, 1000);
}
module.exports = infiniteTimerGame;
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');
describe('infiniteTimerGame', () => {
test('schedules a 10-second timer after 1 second', () => {
const infiniteTimerGame = require('../infiniteTimerGame');
const callback = jest.fn();
infiniteTimerGame(callback);
// At this point in time, there should have been a single call to
// setTimeout to schedule the end of the game in 1 second.
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
// Fast forward and exhaust only currently pending timers
// (but not any new timers that get created during that process)
jest.runOnlyPendingTimers();
// At this point, our 1-second timer should have fired its callback
expect(callback).toBeCalled();
// And it should have created a new timer to start the game over in
// 10 seconds
expect(setTimeout).toHaveBeenCalledTimes(2);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
});
});
Для отладки или по любой другой причине вы можете изменить лимит таймеров, которые будут запущены перед выбросом ошибки:
jest.useFakeTimers({timerLimit: 100});
Передвинуть таймеры на определенное время
Ещё одним вариантом является использование jest.advanceTimersByTime(msToRun). При вызове этого API все таймеры продвинутся на msToRun миллисекунд. Все ожидающие "макро-задачи", которые были помещены в очередь через setTimeout() или setInterval(), и должны быть выполнены в этот период времени, будут выполнены. Кроме того, если эти макро-задачи планируют новые макро-задачи, которые должны быть выполнены в течение того же временного интервала, они будут выполняться до тех пор, пока в очереди не останется больше макро-задач, которые должны быть выполнены в течение msToRun миллисекунд.
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
module.exports = timerGame;
jest.useFakeTimers();
it('calls the callback after 1 second via advanceTimersByTime', () => {
const timerGame = require('../timerGame');
const callback = jest.fn();
timerGame(callback);
// At this point in time, the callback should not have been called yet
expect(callback).not.toBeCalled();
// Fast-forward until all timers have been executed
jest.advanceTimersByTime(1000);
// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
Наконец, иногда в некоторых тестах может быть полезно очистить все ожидающие таймеры. Для этого у нас есть jest.clearAllTimers().
Селективное моделирование
Иногда ваш код может потребовать избежать перезаписи исходной реализации одного или другого API. Если это так, вы можете использовать опцию doNotFake. Например, вот как вы можете предоставить пользовательскую функцию моделирования для performance.mark() в среде jsdom:
/**
* @jest-environment jsdom
*/
const mockPerformanceMark = jest.fn();
window.performance.mark = mockPerformanceMark;
test('allows mocking `performance.mark()`', () => {
jest.useFakeTimers({doNotFake: ['performance']});
expect(window.performance.mark).toBe(mockPerformanceMark);
});
© 2022 Facebook, Inc.
Licensed under the MIT License.
https://jestjs.io/docs/timer-mocks