定时器模拟
原生计时器函数(即 setTimeout()
、setInterval()
、clearTimeout()
、clearInterval()
)对于测试环境而言不太理想,因为它们依赖于实时流逝。Jest 可以用允许你控制时间流逝的功能来替换计时器。很棒的斯科特!
¥The native timer functions (i.e., setTimeout()
, setInterval()
, clearTimeout()
, clearInterval()
) are less than ideal for a testing environment since they depend on real time to elapse. Jest can swap out timers with functions that allow you to control the passage of time. Great Scott!
另请参阅 假定时器 API 文档。
¥Also see Fake Timers API documentation.
启用假定时器
¥Enable Fake Timers
在下面的示例中,我们通过调用 jest.useFakeTimers()
来启用假定时器。这取代了 setTimeout()
和其他定时器功能的原始实现。可以使用 jest.useRealTimers()
将定时器恢复到正常行为。
¥In the following example we enable fake timers by calling jest.useFakeTimers()
. This is replacing the original implementation of setTimeout()
and other timer functions. Timers can be restored to their normal behavior with 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);
});
运行所有计时器
¥Run All Timers
我们可能想要为此模块编写的另一个测试是断言 1 秒后调用回调的测试。为此,我们将使用 Jest 的计时器控制 API 在测试中间快进时间:
¥Another test we might want to write for this module is one that asserts that the callback is called after 1 second. To do this, we're going to use Jest's timer control APIs to fast-forward time right in the middle of the test:
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);
});
运行待处理定时器
¥Run Pending Timers
在某些情况下,你可能有一个递归计时器 - 即在其自己的回调中设置新计时器的计时器。对于这些,运行所有计时器将是一个无限循环,并抛出以下错误:"运行 100000 个计时器后中止,假设无限循环!"
¥There are also scenarios where you might have a recursive timer – that is a timer that sets a new timer in its own callback. For these, running all the timers would be an endless loop, throwing the following error: "Aborting after running 100000 timers, assuming an infinite loop!"
如果是这种情况,使用 jest.runOnlyPendingTimers()
将解决问题:
¥If that is your case, using jest.runOnlyPendingTimers()
will solve the problem:
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);
});
});
出于调试或任何其他原因,你可以更改抛出错误之前运行的计时器的限制:
¥For debugging or any other reason you can change the limit of timers that will be run before throwing an error:
jest.useFakeTimers({timerLimit: 100});
按时间提前定时器
¥Advance Timers by Time
另一种可能性是使用 jest.advanceTimersByTime(msToRun)
。调用此 API 时,所有计时器都会提前 msToRun
毫秒。所有已通过 setTimeout() 或 setInterval() 排队并在此时间范围内执行的待处理 "macro-tasks" 都将被执行。此外,如果这些宏任务调度将在同一时间范围内执行的新宏任务,则这些宏任务将一直执行,直到队列中不再有应在 msToRun 毫秒内运行的宏任务为止。
¥Another possibility is use jest.advanceTimersByTime(msToRun)
. When this API is called, all timers are advanced by msToRun
milliseconds. All pending "macro-tasks" that have been queued via setTimeout() or setInterval(), and would be executed during this time frame, will be executed. Additionally, if those macro-tasks schedule new macro-tasks that would be executed within the same time frame, those will be executed until there are no more macro-tasks remaining in the queue that should be run within msToRun milliseconds.
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()
。
¥Lastly, it may occasionally be useful in some tests to be able to clear all of the pending timers. For this, we have jest.clearAllTimers()
.
选择性伪造
¥Selective Faking
有时,你的代码可能需要避免覆盖一个或另一个 API 的原始实现。如果是这种情况,你可以使用 doNotFake
选项。例如,以下是如何在 jsdom 环境中为 performance.mark()
提供自定义模拟函数:
¥Sometimes your code may require to avoid overwriting the original implementation of one or another API. If that is the case, you can use doNotFake
option. For example, here is how you could provide a custom mock function for performance.mark()
in jsdom environment:
/**
* @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);
});