原生的计时器函数(例如:setTimeout
,setInterval
,clearTimeout
,clearInterval
)对于测试环境不是很理想,因为它们依赖于实时的时间。Jest 允许你使用函数替换掉我们的计时器来控制时间的流逝。
// timerGame.js
'use strict';
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
module.exports = timerGame;
// __tests__/timerGame-test.js
'use strict';
jest.useFakeTimers();
test('waits 1 second before ending the game', () => {
const timerGame = require('../timerGame');
timerGame();
// 判断 setTimeout 方法被调用了一次
expect(setTimeout).toHaveBeenCalledTimes(1);
// 判断最后一次调用 setTimeout 给它传了什么参数。这里是第一个参数是一个函数,第二个参数是 1000
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});
在这里,我们通过调用 jest.useFakeTimers();
来启动伪计时器。这将使用模拟函数模拟 setTimeout 和其他计时器函数。如果在一个文件或 describe
块中运行多个测试,jest.useFakeTimers();
可以每次测试之前手动调用或者使用 beforeEach
设置函数调用。不这样做将导致未重置内部使用计数器。
运行所有定时器
我们可能想为这个模块编写的另一个测试是断言回调在1秒后被调用。为了做到这一点,在测试的中间,我们可以使用 Jest 的计时器控制 API 接口快进⏩时间。
test('calls the callback after 1 second', () => {
const timerGame = require('../timerGame');
const callback = jest.fn();
timerGame(callback);
// 这个时间点,回调函数还没有被调用。因为回调函数是定时一秒后执行
expect(callback).not.toBeCalled();
// 快进⏩,直到所有计时器都执行完毕。(定时时间的快进功能)
jest.runAllTimers();
// 现在回调函数应该被调用了
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
运行挂起的计时器
还有一个场景就是你可能递归地使用了定时器 -- 这有一个定时器在自己的回调里面设置了新的定时器。对于这种情况,运行所有的定时器将是一个无止境的循环...因此,像 jest.runAllTimers
不再有效。对于这些情况,可以使用 jest.runOnlyPendingTimers ()
,仅运行挂起的计时器:
// infiniteTimerGame.js
'use strict';
function infiniteTimerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up! 10 seconds before the next game starts...");
callback && callback();
// 10秒后安排下一场比赛
setTimeout(() => {
infiniteTimerGame(callback);
}, 10000);
}, 1000);
}
module.exports = infiniteTimerGame;
// __tests__/infiniteTimerGame-test.js
'use strict';
jest.useFakeTimers();
describe('infiniteTimerGame', () => {
test('schedules a 10-second timer after 1 second', () => {
const infiniteTimerGame = require('../infiniteTimerGame');
const callback = jest.fn();
infiniteTimerGame(callback);
// 这个点,setTimeout应该已经被调用了一次。
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
// 快进和用尽仅当前挂起的计时器
// (但不是在这个过程中创建的任何新计时器)
jest.runOnlyPendingTimers();
// 此时,我们的1秒定时器应该触发了它的回调
expect(callback).toBeCalled();
// 它应该创建一个新的计时器来在10秒内重新开始游戏
expect(setTimeout).toHaveBeenCalledTimes(2);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
});
});
最后,在某些测试中,清除所有挂起的计时器有时可能很有用。为此,我们使用了jest.clearalltimer()
。
参考
.toHaveBeenCalledTimes(number)
使用这个方法可以确保模拟函数调用的确切次数。
例如,假设你有一个 drinkEach(drink, Array
函数,它接收一个 drink
函数并将应用于传递的数组。你可能想要检查 drink
函数确切的调用次数。你可是使用下面这个测试套件:
test('drinkEach drinks each drink', () => {
const drink = jest.fn();
drinkEach(drink, ['lemon', 'octopus']);
expect(drink).toHaveBeenCalledTimes(2);
});
.toHaveBeenLastCalledWith(arg1, arg2, ...)
如果您有一个模拟函数,您可以使用. toHaveBeenLastCalledWith
来测试最后调用它的参数是什么。例如,假设您有一个applytoallflavor (f)
函数,它将f
应用于许多口味,并且您希望确保在调用它时,它所作用的最后一种口味是“mango”。你可以写:
test('applying to all flavors does mango last', () => {
const drink = jest.fn();
applyToAllFlavors(drink);
expect(drink).toHaveBeenLastCalledWith('mango');
});
jest.useFakeTimers()
指示 Jest 使用标准计时器函数( (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, setImmediate and clearImmediate).)的模拟版本。
jest.useRealTimers()
指示Jest使用标准计时器函数的真实版本。
jest.runAllTimers()
耗尽所有的宏任务队列(即,所有由setTimeout()、setInterval()和setImmediate()以及微任务队列(通常通过process.nextTick在node中接口))
调用此API时,将执行所有挂起的宏任务和微任务。如果这些任务本身调度了新的任务,那么这些任务将不断地耗尽,直到队列中没有剩余的任务为止。
这对于在测试期间同步执行setTimeouts非常有用,以便同步地断言只有在执行setTimeout()或setInterval()回调之后才会发生的一些行为。
jest.runOnlyPendingTimers()
只执行当前挂起的宏任务(即,只包括到目前为止由setTimeout()或setInterval()排队的任务。如果当前挂起的任何宏任务调度了新的宏任务,那么这个调用将不会执行这些新任务。
这对于以下场景非常有用:正在测试的模块递归地调度setTimeout(),而该模块的回调调用setTimeout()调度另一个setTimeout()(这意味着调度永不停止)。在这些场景中,能够一次向前运行一步是很有用的。
jest.clearAllTimers()
从计时器系统中删除任何挂起的计时器。
这意味着,如果任何计时器已经被调度(但是还没有执行),那么它们将被清除,并且在将来永远没有机会执行