非同期処理をJestでテストする
本連載で紹介してきたJestの使い方は、どれも同期的な処理をテストするためのものでした。では、非同期処理を伴うテストケースはどのように作成すればよいのでしょうか。同期的なテストと同じ考え方で記述する分には、リスト3のような書き方ができそうです。
describe("doAsync()", () => { test("処理結果を確認する", () => { doAsync().then(actual => { expect(actual).toBe(1); }) }); });
しかし、この記述ではテストは正しく実施されません。Jestで通常の(同期的な処理をテストするための)スタイルで記述した場合、test
に渡した関数の処理が無事に末尾まで実行された時点で、そのテストケースは成功して完了したものと見なされます。完了したテストケースの中でthen
が呼ばれても、それはもうテスト結果に寄与することができないのです。
ですが、ご安心ください。Jestには、非同期処理の結果を検証するための機能がいくつか整備されているので、解説します。具体的には次の4つの記述スタイルのいずれかを選択して使うことになります。
- コールバックを用いる
- Promiseを返す
- resolve/rejectマッチャを用いる
- async/awaitを用いる
それぞれの記述スタイルについて、解説します。
コールバックを用いる
まずは、もっともシンプルな、コールバックを用いるスタイルです。リスト4の通り記述します。
describe("doAsync()", () => { test("処理結果を確認する", (done) => { // (1) doAsync().then(actual => { expect(actual).toBe(1); done(); // (2) }) }); });
リスト3と処理の流れはほとんど同じですが、テストケースの関数が、done
というコールバック関数を引数に取るという違いがあります(1)。この書き方にすると、(2)のようにdone()
が実行された場合のみテストが成功したと見なされます。done()
が実行されなかった場合は、テストは失敗です。
今回紹介する他の記述スタイルは、すべてPromiseによる非同期処理を前提としているので、Promiseを用いない非同期処理をテストする場合には、必然的にこのスタイルでテストケースを記述することになります。
Promiseを返す
次に、Promiseを返すスタイルを紹介します。リスト5のように記述します。
describe("doAsync()", () => { test("処理結果を確認する", () => { expect.assertions(1); // (2) return doAsync().then(actual => { // (1) expect(actual).toBe(1); }) }); });
(1)のように、テストケースの関数でPromiseをreturnするのが特徴です。このスタイルにおけるテストケースの成功条件は、次の2つです。
-
returnしたPromiseが全体として
resolve
を返すこと - 所定の回数だけマッチャが実行されること
2つめの「所定の回数」とは何のことでしょうか。これは(2)のexpect.assertions
で定義した「マッチャを実行する予定の回数」です。今回の例ではtoBe
を1回だけ実行しているので、expect.assertions(1)
を事前に宣言しました。この回数分だけマッチャが実行されることが、テストケースの成功条件です。
失敗する条件としては、マッチャの実行回数が足りなかった場合や、Promiseがreject
を返した場合があります。
また、このスタイルの特殊な用法として、エラーが発生することをテストケースで確認したい場合に適した書き方があります。Promiseの.catch()
を使う方法です(リスト6)。
describe("doAsync()", () => { test("処理が失敗することを確認する", () => { expect.assertions(1); return doAsync().catch(e => { expect(e).not.toBeNull(); }) }); });
.catch()
には、catch内部でErrorがthrowされない限りはresolve
を返す特性があるので、こういった形での記述も可能になっています。
resolves/rejectsマッチャを用いる
次に、Promiseを用いた別のスタイルとして、resolvesマッチャやrejectsマッチャを使う方法があります(リスト7)。
describe("doAsync()", () => { test("処理結果を確認する", () => { expect.assertions(1); return expect(doAsync()).resolves.toBe(1); // (1) }); test("処理が失敗することを確認する", () => { expect.assertions(1); return expect(doAsync()).rejects.not.toBeNull(); // (2) }); });
このスタイルの特徴は、expect
に非同期処理のPromiseを直接渡す点です(1)。マッチャとして、成功したことを確認したい場合にはresolves
マッチャ、失敗したことを確認したい場合にはrejects
マッチャを続けて記述することによって、Promiseの結果を待つことができます。また、resolves
やrejects
の後ろには通常通りのtoBe
やtoEqual
といったマッチャをつなげることができます。
async/awaitを用いる
最後に、async/awaitを用いるスタイルです。リスト8のように記述します。
describe("doAsync()", () => { test("処理結果を確認する", async () => { // (1) expect.assertions(1); const actual = await doAsync(); // (2) expect(actual).toBe(1); // (3) }); });
特徴としては、(1)に示した通り、test
に渡す関数にasync
を宣言することです。これにより(2)の通り、テストケースの中でawait
を使えるようになります。この記述スタイルであれば、expect
による検証の部分(3)を同期的な処理のテストケースとまったく同じように書けるので、筆者は好んでこのスタイルを使っています。本記事でも、このスタイルで記述していきます。