Timer Mocks
ネイティブのタイマー関数 (i.e., setTimeout
, setInterval
, clearTimeout
, clearInterval
) はテスト環境にとってはあまり理想的ではありません。なぜならそれらの関数は実際の時間経過に依存するからです。 Jest は タイマー関数を自分で時間経過をコントロールできる関数に置き換えることができます。 グレート・スコット!
// timerGame.js 'use strict'; function timerGame(callback) { console.log('Ready....go!'); setTimeout(() => { console.log('Times 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.mock.calls.length).toBe(1); expect(setTimeout.mock.calls[0][1]).toBe(1000); });
ここでは jest.useFakeTimers();
を呼ぶことでフェイクタイマーを有効化しています。これにより setTimeout やその他のタイマー関数はモック関数に置き換えられます。
すべてのタイマーを実行する #
このモジュールに対する別のテストとして引数で渡したコールバック関数が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.mock.calls.length).toBe(1); });
待機中のタイマーを実行する #
別のシナリオとして再帰的タイマーを持っているケースもあります。再帰的タイマーとは自身のコールバック関数の中で新たなタイマーがセットされているタイマーのことです。 このような場合にすべてのタイマーを実行すると無限ループになってしまうため jest.runAllTimers()
のようなやり方は望ましくありません。 このような場合には jest.runOnlyPendingTimers()
が代わりに利用できます。
// infiniteTimerGame.js 'use strict'; function infiniteTimerGame(callback) { console.log('Ready....go!'); setTimeout(() => { console.log('Times 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.mock.calls.length).toBe(1); expect(setTimeout.mock.calls[0][1]).toBe(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.mock.calls.length).toBe(2); expect(setTimeout.mock.calls[1][1]).toBe(10000); }); });
指定時間までタイマーを実行する #
別の可能性としては jest.runTimersToTime(msToRun)
を使うことです。 このAPIが呼ばれた場合、setTimeout() か setInterval() よってキューイングされた msToRun ミリ秒以内の範囲で実行される予定の全ての保留中の "macro-tasks"が、実行されます。 さらに、それらmacro-task自身が新しいmacro-taskを同じ時間枠でスケジュールしている場合は、 msToRun ミリ秒の範囲で実行されるべきmacro-taskがキューから無くなるまで処理されます。
// timerGame.js 'use strict'; function timerGame(callback) { console.log('Ready....go!'); setTimeout(() => { console.log('Times up -- stop!'); callback && callback(); }, 1000); } module.exports = timerGame;
it('calls the callback after 1 second via runTimersToTime', () => { 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.runTimersToTime(1000); // Now our callback should have been called! expect(callback).toBeCalled(); expect(callback.mock.calls.length).toBe(1); });
最後に、保留中のすべてのタイマーをクリアすることはテストによっては役立つことがあります。そのために Jest には jest.clearAllTimers()
があります。
この例のコードは examples/timer で参照できます。