メインコンテンツへスキップ
Version: Next

タイマーモック

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 は タイマー関数を自分で時間経過をコントロールできる関数に置き換えることができます。 グレート・スコット!

info

Also see Fake Timers API documentation.

Enable Fake Timers

In the following example we enable fake timers by calling jest.useFakeTimers(). This is replacing the original implementation of setTimeout() and other timer functions. 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! 10 seconds before the next game starts...");
callback && callback();

// Schedule the next game in 10 seconds
setTimeout(() => {
infiniteTimerGame(callback);
}, 10000);
}, 1000);
}

module.exports = infiniteTimerGame;
__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);
});
});
note

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

指定した時間でタイマーを進める

別の可能性としては jest.advanceTimersByTime(msToRun) を使うことです。 この API が呼び出されると、すべてのタイマーは msToRun ミリ秒で進みます。 この API が呼び出されると、すべてのタイマーは msToRun ミリ秒進みます。 setTimeout() または setInterval() 経由でキューイングされ保留中であった、その時間内に実行予定の "macro-tasks" が実行されます。

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