计时器模拟
原生计时器函数(即 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);
});