タイマーモック
ネイティブタイマー関数(setTimeout()
、setInterval()
、clearTimeout()
、clearInterval()
など)は、実際の時間の経過に依存するため、テスト環境には理想的とは言えません。Jestは、タイマーを時間の経過を制御できる関数に置き換えることができます。グレイト・スコット!
Fake Timers API ドキュメントも参照してください。
フェイクタイマーを有効にする
次の例では、jest.useFakeTimers()
を呼び出すことで、フェイクタイマーを有効にしています。これは、setTimeout()
やその他のタイマー関数の元の 実装を置き換えています。タイマーは jest.useRealTimers()
で通常の動作に戻すことができます。
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
module.exports = timerGame;
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つのテストは、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);
});
保留中のタイマーを実行する
再帰的なタイマー、つまり自身のコールバックで新しいタイマーを設定するタイマーがある場合もあります。このような場合、すべてのタイマーを実行すると無限ループになり、「100000個のタイマーを実行した後に中止します。無限ループと想定されます!」というエラーがスローされます。
それが当てはまる場合は、jest.runOnlyPendingTimers()
を使用すると問題が解決します。
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;
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);
});
});
デバッグなどの理由で、エラーをスローする前に実行されるタイマーの制限を変更できます。
jest.useFakeTimers({timerLimit: 100});
タイマーを時間単位で進める
別の方法として、jest.advanceTimersByTime(msToRun)
を使用する方法があります。このAPIが呼び出されると、すべてのタイマーは msToRun
ミリ秒だけ進められます。 setTimeout() または setInterval() を介してキューに入れられ、この時間枠の間に実行される予定だったすべての保留中の「マクロタスク」が実行されます。さらに、これらのマクロタスクが同じ時間枠内で実行される新しいマクロタスクをスケジュールする場合、msToRunミリ秒以内に実行されるべきキューに残っているマクロタスクがなくなるまで、それらのマクロタスクが実行されます。
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.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.clearAllTimers()
を使用します。
選択的なフェイク
場合によっては、コードで特定のAPIの元の 実装の上書きを回避する必要がある場合があります。その場合は、doNotFake
オプションを使用できます。たとえば、jsdom環境でperformance.mark()
のカスタムモック関数を提供する方法は次のとおりです。
/**
* @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);
});