web-dev-qa-db-ja.com

Jest:TimerとPromiseはうまく機能しません。 (setTimeoutおよびasync関数)

このコードに関するアイデア

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番目の約束を待っている理由だと思います。

19
GutenYe

はい、あなたは正しい軌道に乗っています。


何が起こるか

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

私はちょうど解決策を見つけることができなかったユースケースがあります:

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

基本的に、タイマーが進むまでアクションは解決しません。ここでは循環依存関係のように感じます:解決するために進むにはタイマーが必要です、偽のタイマーは進むために解決する約束が必要です。

1
nemo