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

タイマーモック

ネイティブのタイマー関数 (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();
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 やその他のタイマー関数をモック関数でモックします。 jest.useRealTimers() を使用すると、タイマーは通常の動作に復元できます。

While you can call jest.useFakeTimers() or jest.useRealTimers() from anywhere (top level, inside an it block, etc.), it is a global operation and will affect other tests within the same file. 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:

afterEach(() => {
jest.useRealTimers();
});
test('do something with fake timers', () => {
jest.useFakeTimers();
// ...
});
test('do something with real timers', () => {
// ...
});

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

このモジュールに対する別のテストとして引数で渡したコールバック関数が1秒後に呼ばれたか確認するケースを考えます。 これを実行するには Jest のタイマー管理用の API を使ってテスト中に時間を進めてやります。

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

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

別のシナリオとして再帰的タイマーを持っているケースもあります。 再帰的タイマーとは自身のコールバック関数の中で新たなタイマーがセットされているタイマーのことです。 このような場合にすべてのタイマーを実行すると無限ループになってしまうため 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();
// Schedule the next game in 10 seconds
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);
// 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 it's 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;
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() があります。

この例のコードは examples/timer で参照できます。