Jest - 计时器模拟

原生的计时器函数(例如:setTimeoutsetIntervalclearTimeoutclearInterval)对于测试环境不是很理想,因为它们依赖于实时的时间。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()
从计时器系统中删除任何挂起的计时器。

这意味着,如果任何计时器已经被调度(但是还没有执行),那么它们将被清除,并且在将来永远没有机会执行

你可能感兴趣的:(Jest - 计时器模拟)