Testing Asynchronous Code
JavaScriptではコードを非同期に実行することがよくあります。 非同期的に動作するコードがある場合、Jestはテスト対象のコードがいつ完了したかを別のテストに進む前に知る必要があります。 Jestはこのことを処理する方法をいくつか持っています。
コールバック
最も一般的な非同期処理のパターンはコールバックです。
例えば データを取得してcallback(data)
を呼び出すfetchData(callback)
関数があるとしましょう。 返ってくるデータが'peanut butter'
という文字列であることをテストしたいとします。
デフォルトでは、Jestのテストは一度最後まで実行したら完了します。つまり下記のテストは意図したとおりには動作しないのです。
// Don't do this!
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
また、async
と await
をテストで使用できます。 非同期テストを書くには、 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');
}
});
async
と await
を .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.toThrow('error');
});
これらのケースでは async
や await
は事実上、promiseを使用した例と同じロジックの糖衣構文です。
これらの形式のどれかが他よりも優れているということはなく、コードベースや場合によっては同じファイル内でも混在して合わせて使うことができます。どのスタイルでテストがシンプルになったと感じるかなのです。