計時器模擬
原生計時器函式(例如 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 秒後會呼叫回呼函式。為此,我們將使用 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()
將會解決問題
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).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 毫秒內執行的巨任務為止。
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.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);
});