定时器模拟
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
原生定时器函数(如 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()。
推进到下一帧
在应用程序中,经常需要在动画帧内调度工作(使用 requestAnimationFrame)。我们提供了便捷方法 jest.advanceTimersToNextFrame(),该方法会将所有定时器推进足够毫秒数以执行所有已调度的动画帧。
出于模拟时间的目的,动画帧在计时开始后每 16ms(对应约每秒 60 帧)执行一次。当你在动画帧中调度回调时(使用 requestAnimationFrame(callback)),callback 会在时间推进 16ms 后被调用。jest.advanceTimersToNextFrame() 会将时间推进到下一个 16ms 增量点。如果自动画帧 callback 被调度后时间已推进 6ms,那么时钟将再推进 10ms。
jest.useFakeTimers();
it('calls the animation frame callback after advanceTimersToNextFrame()', () => {
const callback = jest.fn();
requestAnimationFrame(callback);
// At this point in time, the callback should not have been called yet
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersToNextFrame();
// Now our callback should have been called!
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
选择性模拟
有时你的代码可能需要避免覆盖特定 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);
});