このコードに関するアイデア
jest.useFakeTimers()
it('simpleTimer', async () => {
async function simpleTimer(callback) {
await callback() // LINE-A without await here, test works as expected.
setTimeout(() => {
simpleTimer(callback)
}, 1000)
}
const callback = jest.fn()
await simpleTimer(callback)
jest.advanceTimersByTime(8000)
expect(callback).toHaveBeenCalledTimes(9)
}
`` `
で失敗しました
Expected mock function to have been called nine times, but it was called two times.
ただし、LINE-Aからawait
を削除すると、テストに合格します。
PromiseとTimerはうまく機能しませんか?
多分jestが解決する2番目の約束を待っている理由だと思います。
はい、あなたは正しい軌道に乗っています。
await simpleTimer(callback)
は、simpleTimer()
によって返されるPromiseが解決するまで待機するため、callback()
が最初に呼び出され、setTimeout()
も呼び出されます。 jest.useFakeTimers()
setTimeout()
をmock に置き換えたため、モックは[ () => { simpleTimer(callback) }, 1000 ]
で呼び出されたことを記録します。
jest.advanceTimersByTime(8000)
は() => { simpleTimer(callback) }
(1000 <8000以降)を実行し、setTimer(callback)
を呼び出します。これはcallback()
を2回目に呼び出し、await
によって作成されたPromiseを返します。残りのsetTimeout()
がPromiseJobs
queue でキューに入れられ、実行する機会がなかったため、setTimer(callback)
は2回実行されません。
expect(callback).toHaveBeenCalledTimes(9)
は、callback()
が2回しか呼び出されなかったことを報告できません。
これはいい質問です。 JavaScriptのいくつかのユニークな特性と、それが内部でどのように機能するかに注目を集めています。
メッセージキュー
JavaScriptは aメッセージキュー を使用します。ランタイムがキューに戻って次のメッセージを取得する前に、各メッセージは run to completion です。 setTimeout()
のような関数 queue にメッセージを追加します。
ジョブキュー
ES6はJob Queues
を導入し、必要なジョブキューの1つはPromiseJobs
です。これは「Promiseの決済に対する応答であるジョブ」を処理します。このキュー内のジョブは、現在のメッセージが完了した後、次のメッセージが始まる前に実行されます。 then()
は、呼び出されたPromiseが解決されると、PromiseJobs
のジョブをキューに入れます。
async/await
async / await
は、promisesおよびgenerators 上の単なる構文上の砂糖です。 async
は常にPromiseを返し、await
は本質的に、与えられたPromiseにアタッチされたthen
コールバックで残りの関数をラップします。
タイマーモック
Timer Mocks は、 setTimeout()
のような関数をmocks に置き換えて、jest.useFakeTimers()
が呼び出されたときに動作します。これらのモックは、呼び出された引数を記録します。次に、 jest.advanceTimersByTime()
が呼び出されると、コールバックの実行中に追加されるものを含め、経過時間内にスケジュールされたコールバックを同期的に呼び出すループが実行されます。
つまり、setTimeout()
は通常、現在のメッセージが完了するまで実行する前に待機する必要があるメッセージをキューに入れます。タイマーモックを使用すると、現在のメッセージ内でコールバックを同期的に実行できます。
上記の情報を示す例を次に示します。
jest.useFakeTimers();
test('execution order', async () => {
const order = [];
order.Push('1');
setTimeout(() => { order.Push('6'); }, 0);
const promise = new Promise(resolve => {
order.Push('2');
resolve();
}).then(() => {
order.Push('4');
});
order.Push('3');
await promise;
order.Push('5');
jest.advanceTimersByTime(0);
expect(order).toEqual([ '1', '2', '3', '4', '5', '6' ]);
});
Timer Mocksはコールバックを同期的に実行しますが、これらのコールバックにより、ジョブがPromiseJobs
にキューイングされる場合があります。
幸いなことに、PromiseJobs
内のすべての保留中のジョブをasync
テスト内で実行するのは非常に簡単です。必要なのは、await Promise.resolve()
を呼び出すだけです。これは基本的に、テストの残りをPromiseJobs
キューの最後にキューイングし、キュー内のすべてを最初に実行させます。
それを念頭に置いて、テストの作業バージョンを以下に示します。
jest.useFakeTimers()
it('simpleTimer', async () => {
async function simpleTimer(callback) {
await callback();
setTimeout(() => {
simpleTimer(callback);
}, 1000);
}
const callback = jest.fn();
await simpleTimer(callback);
for(let i = 0; i < 8; i++) {
jest.advanceTimersByTime(1000);
await Promise.resolve(); // allow any pending jobs in the PromiseJobs queue to run
}
expect(callback).toHaveBeenCalledTimes(9); // SUCCESS
});
私はちょうど解決策を見つけることができなかったユースケースがあります:
function action(){
return new Promise(function(resolve, reject){
let poll
(function run(){
callAPI().then(function(resp){
if (resp.completed) {
resolve(response)
return
}
poll = setTimeout(run, 100)
})
})()
})
}
テストは次のようになります。
jest.useFakeTimers()
const promise = action()
// jest.advanceTimersByTime(1000) // this won't work because the timer is not created
await expect(promise).resolves.toEqual(({completed:true})
// jest.advanceTimersByTime(1000) // this won't work either because the promise will never resolve
基本的に、タイマーが進むまでアクションは解決しません。ここでは循環依存関係のように感じます:解決するために進むにはタイマーが必要です、偽のタイマーは進むために解決する約束が必要です。