メインコンテンツへスキップ
Version: 26.x

タイマーモック

ネイティブのタイマー関数 (i.e., 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(); // or you can set "timers": "fake" globally in configuration file

test('waits 1 second before ending the game', () => {
const timerGame = require('../timerGame');
timerGame();

expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});

ここでは、 jest.useFakeTimers(); を呼び出して偽のタイマーを有効にしています。 setTimeout やその他のタイマー関数をモック関数でモックします。 1つのファイル中やdescribeブロック中のテストを複数実行する場合、jest.useFakeTimers(); は各テストごとに手動で呼び出すか、beforeEachのようなセットアップ関数で呼び出すことになります。 そうしないと内部で使用するカウンターがリセットされません。

All of the following functions need fake timers to be set, either by jest.useFakeTimers() or via "timers": "fake" in the config file.

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

すべてのタイマーを実行する

このモジュールに対する別のテストとして引数で渡したコールバック関数が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.toBeCalled();

// Fast-forward until all timers have been executed
jest.runAllTimers();

// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});

待機中のタイマーを実行する

別のシナリオとして再帰的タイマーを持っているケースもあります。 再帰的タイマーとは自身のコールバック関数の中で新たなタイマーがセットされているタイマーのことです。 For these, running all the timers would be an endless loop, throwing the following error:

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.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();

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

module.exports = infiniteTimerGame;
__tests__/infiniteTimerGame-test.js
'use strict';

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).toBeCalled();

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

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

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

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
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.toBeCalled();

// Fast-forward until all timers have been executed
jest.advanceTimersByTime(1000);

// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});

最後に、保留中のすべてのタイマーをクリアすることはテストによっては役立つことがあります。 そのために Jest には jest.clearAllTimers() があります。

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