非同期コードのテスト
JavaScriptではコードを非同期に実行することがよくあります。 非同期的に動作するコードがある場合、Jestはテスト対象のコードがいつ完了したかを別のテストに進む前に知る必要があります。 Jestはこのことを処理する方法をいくつか持っています。
Promises
テストからpromiseを返すと、Jestはそのpromiseがresolveされるまで待機します。 promiseがrejectされると、テストが失敗します。
例えば、fetchData
が'peanut butter'
という文字列でresolveされるpromiseを返すとします。 以下のようにテストすることができます:
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
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.toMatch('error');
});
これらのケースでは async
や await
は事実上、promiseを使用した例と同じロジックの糖衣構文です。
promiseを返す、またはawait
するようにしましょう。return
またはawait
を省いた場合、fetchData
から返されるpromiseがresolveまたはrejectされる前に、テストが終了してしまいます。
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'));
});
コールバック
promiseを使わない場合、コールバックが使えます。 例えば、fetchData
がpromiseを返すのではなく、コールバックを使うとします。つまり、データを取得し終わったら、callback(null, data)
を呼ぶとします。 返ってくるデータが'peanut butter'
という文字列であることをテストしたいとします。
デフォルトでは、Jestのテストは一度最後まで実行したら完了します。 つまり下記のテストは意図したとおりには動作しないのです。
// 実行しないでください!
test('the data is peanut butter', () => {
function callback(error, data) {
if (error) {
throw error;
}
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
問題はfetchData
が完了した時点でテストも完了してしまい、コールバックが呼ばれないことです。
これを修正する別の形のtest
があります。 テストを空の引数の関数の中に記述するのではなく、 done
という1つの引数を利用します。 Jestは テストを終了する前に、done
コールバックが呼ばれるまで待ちます。
test('the data is peanut butter', done => {
function callback(error, data) {
if (error) {
done(error);
return;
}
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
done()
が呼ばれない場合、お望み通りにテストが(タイムアウトにより)失敗します。
expect
文が失敗した場合、エラーがスローされて done()
は呼び出されません。 テストログで失敗した理由を確認したい場合。 try
ブロックでexpect
をラップし、 catch
ブロック内でエラーを done
に渡す必要があります。 そうしなければ、 expect(data)
によってどの値が受信されたかを示さない不透明なタイムアウトエラーが起こるだけになります。
done()
should not be mixed with promises as this tends to lead to memory leaks in your tests.
.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');
});
これらの形式のどれかが他よりも優れているということはなく、コードベースや場合によっては同じファイル内でも混在して合わせて使うことができます。 どのスタイルでテストがシンプルになったと感じるかなのです。