タイマーモック
ネイティブのタイマー関数 (i.e., setTimeout
, setInterval
, clearTimeout
, clearInterval
) はテスト環境にとってはあまり理想的ではありません。 Jest は タイマー関数を自分で時間経過をコントロールできる関数に置き換えることができます。 グレート・スコット!
'use strict';
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
module.exports = timerGame;
'use strict';
jest.useFakeTimers(); // or you can set "timers": "fake" globally in configuration file
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);
});
ここでは、 jest.useFakeTimers();
を呼び出して偽のタイマーを有効にしています。 setTimeout
やその他のタイマー関数をモック関数でモックします。 jest.useRealTimers()
を使用すると、タイマーは通常の動作に復元できます。
jest.useFakeTimers()
やjest.useRealTimers()
はどこからでも (トップレベルやit
ブロック内など) 呼び出すことができますが、これはグローバル操作であり、同じファイル内の他のテストにも影響します。 テストごとに内部カウンタをリセットするには jest.useFakeTimers()
を呼び出す必要があります。 すべてのテストで偽のタイマーを使用するわけではない場合は、手動でクリーンアップする必要があります。 そうでないと偽のタイマーがテスト間で漏洩してしまいます。
afterEach(() => {
jest.useRealTimers();
});
test('do something with fake timers', () => {
jest.useFakeTimers();
// ...
});
test('do something with real timers', () => {
// ...
});
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 を使ってテスト中に時間を進めてやります。
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()
が代わりに利用できます。
'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;
'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" が実行されます。
'use strict';
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
module.exports = timerGame;
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.