跳转至主内容
版本:下一个

计时器模拟

原生的计时器函数(如:setTimeout(), setInterval(), clearTimeout(), clearInterval())并不是很方便测试,因为程序需要等待相应的延时。 Jest可以通过一个函数转换计时器以便允许你控制时间流量。 Great Scott!

信息

另请参阅假计时器 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);
});

运行等待计时器

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!"

If that is your case, using jest.runOnlyPendingTimers() will solve the problem:

infiniteTimerGame.js
function infiniteTimerGame(callback) {
console.log('Ready....go!');

setTimeout(() => {
console.log("Time's up! expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
__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);
});
});
备注

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});

使用时间的高级计时器

另一种可选方式是使用 jeste. advancertimersbytime (msToRun)。 通过调用msToRun这个 API时, 所有计时器都将以毫秒增长。 When this API is called, all timers are advanced by msToRun milliseconds. 所有通过setTimeout() 或setInterval() 而处于任务队列中等待中的“宏任务”和一切其他应该在本时间片中被执行的东西都应该被执行。 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.

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()。 For this, we have jest.clearAllTimers().

Advance Timers to the next Frame

In applications, often you want to schedule work inside of an animation frame (with requestAnimationFrame). We expose a convenience method jest.advanceTimersToNextFrame() to advance all timers enough milliseconds to execute all actively scheduled animation frames.

For mock timing purposes, animation frames are executed every 16ms (mapping to roughly 60 frames per second) after the clock starts. When you schedule a callback in an animation frame (with requestAnimationFrame(callback)), the callback will be called when the clock has advanced 16ms. jest.advanceTimersToNextFrame() will advance the clock just enough to get to the next 16ms increment. If the clock has already advanced 6ms since a animation frame callback was scheduled, then the clock will be advanced by 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);
});

Selective Faking

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);
});