メインコンテンツへスキップ
Version: 27.2

非同期コードのテスト

JavaScriptではコードを非同期に実行することがよくあります。 非同期的に動作するコードがある場合、Jestはテスト対象のコードがいつ完了したかを別のテストに進む前に知る必要があります。 Jestはこのことを処理する方法をいくつか持っています。

コールバック#

最も一般的な非同期処理のパターンはコールバックです。

例えば データを取得してcallback(data)を呼び出すfetchData(callback)関数があるとしましょう。 返ってくるデータが'peanut butter'という文字列であることをテストしたいとします。

デフォルトでは、Jestのテストは一度最後まで実行したら完了します。 つまり下記のテストは意図したとおりには動作しないのです。

// 実行しないでください!
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});

問題はfetchDataが完了した時点でテストも完了してしまい、コールバックが呼ばれないことです。

これを修正する別の形のtest があります。 テストを空の引数の関数の中に記述するのではなく、 doneという1つの引数を利用します。 Jestは テストを終了する前に、done コールバックが呼ばれるまで待ちます。

test('the data is peanut butter', done => {
function callback(data) {
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});

done() が呼ばれない場合、お望み通りにテストが(タイムアウトにより)失敗します。

expect 文が失敗した場合、エラーがスローされて done() は呼び出されません。 テストログで失敗した理由を確認したい場合。 tryブロックでexpect をラップし、 catch ブロック内でエラーを done に渡す必要があります。 そうしなければ、 expect(data) によってどの値が受信されたかを示さない不透明なタイムアウトエラーが起こるだけになります。

Promises#

promiseを使用するコードであれば、非同期テストをもっと簡単に処理する方法があります。 テストからpromiseを返すと、Jestはそのpromiseがresolveされるまで待機します。 promiseがrejectされた場合は、テストは自動的に失敗します。

例えば、fetchDataにおいて、コールバックを使用する代わりに 'peanut butter'文字列を返すと思われるpromiseを返すことにしましょう。 以下のようにテストすることができます:

test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});

promiseを返していることを確認してください。 - もしこの return 文を省略した場合、あなたのテストは、fetchDataがresolveされpromiseが返ってくる前に実行され、then() 内のコールバックが実行される前に完了してしまいます。

promiseがrejectされることを期待するケースでは .catch メソッドを使用してください。 想定した数のアサーションが呼ばれたことを確認するため、expect.assertionsを必ず追加して下さい。 Otherwise, a fulfilled promise would not fail the test.

test('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(e => expect(e).toMatch('error'));
});

.resolves / .rejects#

expect宣言で .resolves マッチャを使うこともでき、Jestはそのpromiseが解決されるまで待機します。 promiseがrejectされた場合は、テストは自動的に失敗します。

test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
  • もしこの return 文を省略した場合、あなたのテストは、fetchDataがresolveされpromiseが返ってくる前に実行され、then() 内のコールバックが実行される前に完了してしまいます。

promiseがrejectされることを期待するケースでは.rejects マッチャを使用してください。 .resolvesマッチャと似た動作をします。 promiseが成功した場合は、テストは自動的に失敗します。

test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});

Async/Await#

また、asyncawaitをテストで使用できます。 非同期テストを書くには、 testに渡す関数の前にasync キーワードを記述するだけです。 例えば、同じfetchData シナリオは次のようにテストできます:

test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
expect(e).toMatch('error');
}
});

asyncawait.resolves または .reject と組み合わせることができます。

test('the data is peanut butter', async () => {
await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
await expect(fetchData()).rejects.toMatch('error');
});

これらのケースでは asyncawait は事実上、promiseを使用した例と同じロジックの糖衣構文です。

これらの形式のどれかが他よりも優れているということはなく、コードベースや場合によっては同じファイル内でも混在して合わせて使うことができます。 どのスタイルでテストがシンプルになったと感じるかなのです。