🌊

jest

2024/07/16に公開

waitForとは?

非同期処理を待ってからアサーションを行う
非同期で何らかの処理を行った後アサーションを行う場合、非同期の処理が終わる前に実行されて期待した結果が得られない場合があります。
waitForを使うことで、非同期の処理が終わるのを待ってアサーションを行うことができます。
// 非同期での処理を行なう…

// 非同期処理の終了を待って引数に渡した処理を実行する
await waitFor(
() => screen.getByRole('dialog', { name: 'XX確認' })
)

https://zenn.dev/spacemarket/articles/6b52d53696ef13

Jest Promise
非同期コードのテスト

テストから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 (error) {
expect(error).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 を使用した例と同じロジックの糖衣構文です。
CAUTION
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(error => expect(error).toMatch('error'));
});
Promiseがreejctされることを期待するには.catchメソッドを利用?

コールバック

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) によってどの値が受信されたかを示さない不透明なタイムアウトエラーが起こるだけになります。
CAUTION
Jest will throw an error, if the same test function is passed a done() callback and returns a promise. This is done as a precaution to avoid 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');
    });

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

これではダメなのか?
→このメソッドの問題点は、プロミスが解決されれば、テストはリジェクトされると思っていてもパスしてしまうことだ。

https://github.com/jestjs/jest/issues/2129

このissueでは、JestのテストでPromiseの拒否を正しくテストする方法について話し合っています。具体的には、非同期関数が期待通りにエラーをスローしない場合にテストを失敗させる方法について議論されています。
主な議論のポイント

  1. fail() 関数の提案
    • excitement-engineerが、非同期関数がエラーをスローしなかった場合にテストを失敗させるために fail() 関数を使う提案をしました。
    • これに対して thymikee は、代わりに toThrow() メソッドを使うことを提案しましたが、excitement-engineer は toThrow() がPromiseの拒否をチェックするには適していないと反論しました。
  2. Promiseの拒否をテストする方法
    • thymikee は、Promiseの拒否をテストするための具体的な例を示しましたが、excitement-engineer はその方法ではPromiseが解決された場合にテストが通過してしまうと指摘しました。
    • 新しいマッチャー .toBeRejected(object | string) の導入を提案し、それによってテストをより明確にする方法について話し合いました。
  3. 他の意見と代替案
    • 他のユーザーも議論に参加し、 done.fail() や expect.assertions(1) などの方法を提案しました。
    • これらの方法の利点と欠点についての意見交換が行われました。
      具体的な情報
  • fail() 関数: getUserName() がエラーをスローしなかった場合にテストを失敗させるために使用。
  • toThrow() メソッド: getUserName 関数がエラーをスローするかどうかをチェックするために使用。
  • done.fail() メソッド: 非同期コードで特定の条件が発生した場合にテストを失敗させるために使用。
  • expect.assertions(1): アサーションが実行されなかった場合にテストを失敗させるために使用。
  • 新しいマッチャーの提案: .toBeRejected(object | string) や .toBeRejectedWithSnapshot()。

結論
議論の結果、excitement-engineer の提案した fail() 関数を使う方法は、Jestのドキュメント化されたAPIではサポートされていないが、一部のユーザーには便利であると認識されています。一方、より標準的な方法としては expect.assertions(1) を使うか、新しいマッチャーの導入を検討することが推奨されています。

expect.arrayContaining(array)

expect.arrayContaining(array) は受け取った配列が期待される配列の要素全てを含む場合に一致します。 つまり受け取った配列は期待される配列を 包含 するということです。 したがって受け取る配列が期待される配列に含まれない要素を含んでいても一致します。
以下のケースでリテラル値の代わりに使用できます:

  • in toEqual or toHaveBeenCalledWith
  • to match a property in objectContaining or toMatchObject

これはつまり、 完全一致ではないと言う事

describe('果物バスケットのテスト', () => {
    const 期待する果物 = ['りんご', 'バナナ'];
    
    it('期待する果物が全て入っているバスケットはOK', () => {
        const 実際のバスケット = ['りんご', 'バナナ', 'オレンジ'];
        expect(実際のバスケット).toEqual(expect.arrayContaining(期待する果物));
    });
    
    it('期待する果物が足りないバスケットはNG', () => {
        const 不完全なバスケット = ['りんご', 'オレンジ'];
        expect(不完全なバスケット).not.toEqual(expect.arrayContaining(期待する果物));
    });
});```

Discussion