Async/Await を使用した Promise の副作用のテスト

フレームワークのコールバック内で非同期コードを呼び出しているときに、その副作用をテストする必要がある状況に遭遇したことがあるかもしれません。 たとえば、React コンポーネントの内部で API 呼び出しを行っている可能性があります。 componentDidMount() 次に呼び出すコールバック setState() リクエストが完了し、コンポーネントが特定の状態にあることをアサートしたい場合。 この記事では、このような種類のシナリオをテストするためのテクニックを示します。
単純化した例を挙げてみましょう。 というクラスがあります PromisesHaveFinishedIndicator。 コンストラクターは Promise のリストを受け取ります。 すべての Promise が解決されると、インスタンスの finished プロパティがに設定されています true:

class PromisesHaveFinishedIndicator {constructor(promises) { this.finished = false; Promise.all(promises).then(() => { this.finished = true; }); } }

良いテスト ケースには、解決タイミングを制御できる複数の Promise を指定してコンストラクターを呼び出し、その値の期待値を記述することが含まれます。 this.finished それぞれの約束が解決されるにつれて。
テストでの Promise の解決タイミングを制御するために、次を使用します。 Deferred を公​​開するオブジェクト resolve & reject メソッド:

class Deferred {constructor() { this.promise = new Promise((resolve,拒否) => { this.resolve = 解決; this.reject =拒否; }); } }

これにより、次のテストをセットアップできます。 PromisesHaveFinishedIndicator。 私たちは 冗談 この例ではテスト フレームワークを使用していますが、この手法は他のテスト フレームワークにも適用できます。

test('すべての Promise が解決された後、終了を true に設定します', () => { const d1 = new Deferred(); const d2 = new Deferred(); const インジケータ = new PromisesHaveFinishedIndicator([d1.promise, d2.promise] ); Expect(indicator.finished).toBe(false); d2.resolve(); Expect(indicator.finished).toBe(false); d1.resolve(); Expect(indicator.finished).toBe(true); });

Promise コールバックは非同期であるため、このテストは実際には失敗します。そのため、キューにあるコールバックはすべて、このテストの最後のステートメントの後に実行されます。 完了まで実行する セマンティクス。 言い換えれば、 Promise.all コール: () => { this.finished = true; } このテストはすでに終了した後に実行されます。
Jest (および他のテスト フレームワーク) は、最後のステートメントの後にテストが終了しないようにすることで、非同期に対処する方法を提供します。 提供されたものを呼び出す必要があります done テストが終了したことをランナーに伝えるための機能です。 さて、次のようなものがうまくいくと思うかもしれません:

test('すべての Promise が解決された後、終了を true に設定します', (done) => { const d1 = new Deferred(); const d2 = new Deferred(); const インジケータ = new PromisesHaveFinishedIndicator([d1.promise, d2.promise ]); Expect(indicator.finished).toBe(false); d2.resolve(); d2.then(() => { Expect(indicator.finished).toBe(false); d1.resolve(); d1. then(() => { Expect(indicator.finished).toBe(true); done(); }); }); });

ただし、これも失敗します。 その理由は実装にあります Promise.all。 時 resolve 呼び出されます d1 (と d2 同じように)、 Promise.all すべての Promise が解決されたかどうかを確認するコールバックをスケジュールします。 このチェックが true を返した場合、から返された Promise が解決されます。 Promise.all を呼び出すと、 () => { this.finished = true; } 折り返し電話。 このコールバックは、この時点ではまだキューに残っています。 done 呼ばれます!
ここで問題は、設定するコールバックをどのように作成するかです。 this.finished 〜へ true 電話をかける前に実行する done? これに答えるには、Promise が解決または拒否されたときに Promise コールバックがどのようにスケジュールされるかを理解する必要があります。 ジェイク・アーチボルドの記事 タスク、マイクロタスク、キュー、スケジュール はまさにそのテーマについて詳しく説明されているので、一読することを強くお勧めします。
要約: Promise コールバックはマイクロタスク キューに入れられ、API のコールバックは次のようになります。 setTimeout(fn) & setInterval(fn) マクロタスクキューに入れられます。 マイクロタスク キューにあるコールバックは、スタックが空になった直後に実行され、マイクロタスクが別のマイクロタスクをスケジュールすると、それらはマクロタスク キューに譲る前に継続的にキューから取り出されます。
この知識があれば、次のようにしてこのテストに合格できます。 setTimeout then():

test('すべての Promise が解決された後、終了を true に設定します', (done) => { const d1 = new Deferred(); const d2 = new Deferred(); const インジケータ = new PromisesHaveFinishedIndicator([d1.promise, d2.promise ]); Expect(indicator.finished).toBe(false); d2.resolve(); setTimeout(() => { Expect(indicator.finished).toBe(false); d1.resolve(); setTimeout(() => { Expect(indicator.finished).toBe(true); done(); }, 0); }, 0); });

これが機能する理由は、XNUMX 番目の時点までに、 setTimeout コールバックが実行されると、次の Promise コールバックが実行されたことがわかります。

  • 実装内のコールバック Promise.all すべての Promise が解決されたことを確認し、返された Promise を解決します。
  • 設定するコールバック this.finished = true.

たくさん持っている setTimeout(fn, 0) 私たちのコードは、控えめに言っても見苦しいです。 新しいものでこれをクリーンアップできます async/await 構文:

function flashPromises() { return new Promise((resolve,拒否) => setTimeout(resolve, 0)); } test('すべての Promise が解決された後に終了を true に設定', async () => { const d1 = new Deferred(); const d2 = new Deferred(); constインジケーター = new PromisesHaveFinishedIndicator([d1.promise, d2.約束]); 期待(indicator.finished).toBe(false); d2.resolve(); await flashPromises(); Expect(indicator.finished).toBe(false); d1.resolve(); await flashPromises(); Expect(indicator.finished).toBe(true); });

さらに派手になりたい場合は、次を使用できます setImmediate setTimeout 一部の環境 (Node.js) では。 より速いです setTimeout ただし、マイクロタスクの後にも実行されます。

function flashPromises() { return new Promise(resolve => setImmediate(resolve)); }

Promise と非同期を含むテストを作成する場合、コールバックがどのようにスケジュールされるか、およびさまざまなキューがイベント ループで果たす役割を理解することが有益です。 この知識があれば、作成する非同期コードを推論できるようになります。

類似の投稿