跳至主要內容
版本:29.7

計時器模擬

原生計時器函式(例如 setTimeout()setInterval()clearTimeout()clearInterval())對於測試環境而言並非理想,因為它們依賴於實際時間流逝。Jest 可以使用允許您控制時間流逝的函式替換計時器。 太棒了!

資訊

另請參閱 偽造計時器 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);
});

執行所有計時器

我們可能想要為這個模組撰寫的另一個測試,是斷言在 1 秒後會呼叫回呼函式。為此,我們將使用 Jest 的計時器控制 API 在測試過程中快轉時間

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.toHaveBeenCalled();

// Fast-forward until all timers have been executed
jest.runAllTimers();

// Now our callback should have been called!
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledTimes(1);
});

執行待處理計時器

還有一些情況,你可能有一個遞迴計時器,也就是在自己的回呼函式中設定新計時器的計時器。對於這些情況,執行所有計時器將會形成無窮迴圈,並拋出以下錯誤:「在執行 100000 個計時器後中止,假設為無限迴圈!」

如果是這種情況,使用 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();

// Schedule the next game in 10 seconds
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);

// 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).toHaveBeenCalled();

// 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 毫秒內執行的巨任務為止。

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.toHaveBeenCalled();

// Fast-forward until all timers have been executed
jest.advanceTimersByTime(1000);

// Now our callback should have been called!
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledTimes(1);
});

最後,在某些測試中,偶爾可能需要清除所有待處理的計時器。針對此情況,我們有 jest.clearAllTimers()

選擇性偽造

有時你的程式碼可能需要避免覆寫一個或另一個 API 的原始實作。如果是這種情況,你可以使用 doNotFake 選項。例如,以下是如何在 jsdom 環境中為 performance.mark() 提供自訂模擬函式

/**
* @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);
});