Перейти до основного змісту
Версія: 29.1

Імітації таймерів

Глобальні функції-таймери (наприклад, setTimeout(), setInterval(), clearTimeout(), clearInterval()) є проблемними для тестового середовища, оскільки вони спираються на реальний час. Jest вміє підміняти їх, щоб ви могли керувати часом. Great Scott!

info

Також див. документацію API імітацій таймерів.

Ввімкнення фіктивних таймерів

В прикладі нижче, викликаючи jest.useFakeTimers(), ми вмикаємо фіктивні таймери. Це замінює оригінальну реалізацію setTimeout() та інші функції-таймери. Функції-таймери можна повернути до своєї оригінальної поведінки викликом jest.useRealTimers().

timerGame.js
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}

module.exports = timerGame;
__tests__/timerGame-test.js
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);
});

Запуск усіх таймерів

Інший тест, який ми можемо написати для цього модуля, це той, що перевіряє, що callback викликається через 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);
});

Запуск активних таймерів

Є також сценарії, коли ви можете мати рекурсивний таймер – це таймер, який запускає новий таймер у власному зворотному виклику. Через це, запуск всіх таймерів був би нескінченним циклом, що кидав наступну помилку: "Аборт після виконання 100 000 таймерів через припущення, що це нескінченний цикл!"

Якщо це ваш випадок, використання jest.runOnlyPendingTimers() розв'яже проблему:

infiniteTimerGame.js
function infiniteTimerGame(callback) {
console.log('Ready....go!');

setTimeout(() => {
console.log("Time's up! 10 seconds before the next game starts...");
callback && callback();

// Запланувати наступну гру через 10 секунд
setTimeout(() => {
infiniteTimerGame(callback);
}, 10000);
}, 1000);
}

module.exports = infiniteTimerGame;
__tests__/infiniteTimerGame-test.js
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);

// На цей момент, мав статись один виклик
// setTimeout, аби запланувати кінець гри через 1 секунду.
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

// Промотати і відпрацювати тільки активні таймери
// (але не будь-які нові таймери, які будуть створені протягом цієї операції)
jest.runOnlyPendingTimers();

// На цей момент, наш секундний таймер має викликати свій callback
expect(callback).toBeCalled();

// І створити новий таймер, щоб почати гру знову через 10 секунд
expect(setTimeout).toHaveBeenCalledTimes(2);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
});
});
note

Для відлагодження чи з будь-якої іншої причини, ви можете змінити ліміт таймерів, що будуть виконані перед викидом помилки:

jest.useFakeTimers({timerLimit: 100});

Наближення таймерів у часі

Інша можливість - використовувати jest.advanceTimersByTime(msToRun). Коли викликається цей метод, всі таймери наближаються на msToRun мілісекунд. Всі макрозавдання заплановані через setTimeout() або setInterval() до виконання протягом цього періоду часу, будуть виконані. Крім того якщо ці макрозавдання запланують нові, які мають виконатися в межах того ж часового проміжку, вони теж будуть виконані допоки в черзі не залишиться завдань, що мають бути виконані впродовж msToRun мілісекунд.

timerGame.js
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}

module.exports = timerGame;
__tests__/timerGame-test.js
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);
});