计时器模拟

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

timerGame.js

  1. 'use strict';
  2. function timerGame(callback) {
  3. console.log('Ready....go!');
  4. setTimeout(() => {
  5. console.log("Time's up -- stop!");
  6. callback && callback();
  7. }, 1000);
  8. }
  9. module.exports = timerGame;

__tests__/timerGame-test.js

  1. 'use strict';
  2. jest.useFakeTimers();
  3. jest.spyOn(global, 'setTimeout');
  4. test('waits 1 second before ending the game', () => {
  5. const timerGame = require('../timerGame');
  6. timerGame();
  7. expect(setTimeout).toHaveBeenCalledTimes(1);
  8. expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
  9. });

Here we enable fake timers by calling jest.useFakeTimers(). This mocks out setTimeout and other timer functions with mock functions. Timers can be restored to their normal behavior with jest.useRealTimers(). 这个模拟出 setTiimeout 其他计时器函数也将使用模拟的方法。 计时器可以恢复他们默认的行为通过jest.useRealTimers().

您可以从任何地方调用 jest.useFakeTimers()jest.useRealTimers()(顶层,it内部块,等),它是一个 全局操作 ,并将影响到同一文件中的其他测试用例。 Additionally, you need to call jest.useFakeTimers() to reset internal counters before each test. If you plan to not use fake timers in all your tests, you will want to clean up manually, as otherwise the faked timers will leak across tests:

  1. afterEach(() => {
  2. jest.useRealTimers();
  3. });
  4. test('do something with fake timers', () => {
  5. jest.useFakeTimers();
  6. // ...
  7. });
  8. test('do something with real timers', () => {
  9. // ...
  10. });
  11. });
  12. test('do something with real timers', () => {
  13. // ...
  14. });

Currently, two implementations of the fake timers are included - modern and legacy, where modern is the default one. See configuration for how to configure it.

运行所有计时器

对于这个模块我们还需要写一个测试,用于判断回调函数是否在1秒后被调用的。 为此,我们将使用Jest的定时器控制API,用于在测试中将时间“快进”到正确的时间点。

  1. jest.useFakeTimers();
  2. test('calls the callback after 1 second', () => {
  3. const timerGame = require('../timerGame');
  4. const callback = jest.fn();
  5. timerGame(callback);
  6. // At this point in time, the callback should not have been called yet
  7. expect(callback).not.toBeCalled();
  8. // Fast-forward until all timers have been executed
  9. jest.runAllTimers();
  10. // Now our callback should have been called!
  11. expect(callback).toBeCalled();
  12. expect(callback).toHaveBeenCalledTimes(1);
  13. });

运行等待计时器

在某些场景下你可能还需要“循环定时器”——在定时器的callback函数中再次设置一个新定时器。 For these, running all the timers would be an endless loop, throwing the following error:

  1. Ran 100000 timers, and there are still more! Assuming we've hit an infinite recursion and bailing out...

So something like jest.runAllTimers() is not desirable. 对于这种情况,如果将定时器一直运行下去那将陷入死循环,所以在此场景下不应该使用jest.runAllTimers() For these cases you might use jest.runOnlyPendingTimers():

infiniteTimerGame.js

  1. 'use strict';
  2. function infiniteTimerGame(callback) {
  3. console.log('Ready....go!');
  4. setTimeout(() => {
  5. console.log("Time's up! expect(callback).toBeCalled();
  6. expect(callback).toHaveBeenCalledTimes(1);
  7. });

__tests__/infiniteTimerGame-test.js

  1. 'use strict';
  2. jest.useFakeTimers();
  3. jest.spyOn(global, 'setTimeout');
  4. describe('infiniteTimerGame', () => {
  5. test('schedules a 10-second timer after 1 second', () => {
  6. const infiniteTimerGame = require('../infiniteTimerGame');
  7. const callback = jest.fn();
  8. infiniteTimerGame(callback);
  9. // At this point in time, there should have been a single call to
  10. // setTimeout to schedule the end of the game in 1 second.
  11. expect(setTimeout).toHaveBeenCalledTimes(1);
  12. expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
  13. // Fast forward and exhaust only currently pending timers
  14. // (but not any new timers that get created during that process)
  15. jest.runOnlyPendingTimers();
  16. // At this point, our 1-second timer should have fired its callback
  17. expect(callback).toBeCalled();
  18. // And it should have created a new timer to start the game over in
  19. // 10 seconds
  20. expect(setTimeout).toHaveBeenCalledTimes(2);
  21. expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
  22. });
  23. });

使用时间的高级计时器

另一种可选方式是使用 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

  1. 'use strict';
  2. function timerGame(callback) {
  3. console.log('Ready....go!');
  4. setTimeout(() => {
  5. console.log("Time's up -- stop!");
  6. callback && callback();
  7. }, 1000);
  8. }
  9. module.exports = timerGame;

__tests__/timerGame-test.js

  1. jest.useFakeTimers();
  2. it('calls the callback after 1 second via advanceTimersByTime', () => {
  3. const timerGame = require('../timerGame');
  4. const callback = jest.fn();
  5. timerGame(callback);
  6. // At this point in time, the callback should not have been called yet
  7. expect(callback).not.toBeCalled();
  8. // Fast-forward until all timers have been executed
  9. jest.advanceTimersByTime(1000);
  10. // Now our callback should have been called!
  11. expect(callback).toBeCalled();
  12. expect(callback).toHaveBeenCalledTimes(1);
  13. });

最后,在某些测试中你可能需要清除所有等待状态下的定时器,为此,可以使用 jest.clearAllTimers()。 For this, we have jest.clearAllTimers().

The code for this example is available at examples/timer.