[備忘録] 同期関数/非同期関数で期待通りの例外がスローされたことをJestで評価する

2023.03.02

こんにちは、CX事業本部 Delivery部の若槻です。

今回は、同期関数/非同期関数で期待通りの例外がスローされたことをJestで評価することがよくあるのですが、その際のテストの記述をよく忘れてはググっているので、備忘としてまとめておきます。

先にまとめ

同期関数/非同期関数の例外のスローは、それぞれ次の記述でテストできます。

test('_syncFunc', () => {
  //同期関数のテスト
  expect(() => syncFunc()).toThrow(new Error('syncFunc failed.'));
});

test('_asyncFunc', async () => {
  //非同期関数のテスト
  await expect(asyncFunc()).rejects.toThrow(new Error('asyncFunc failed.'));
});

解説

Jestで呼び出された関数が例外をスローしたことを評価したい場合は、.toThrow(error?)を使用します。

Use .toThrow to test that a function throws when it is called.

また、JavaScriptの非同期関数は実行結果としてPromiseを返します。

Promiseがreject(例外をスロー)することを期待したテストを実施したい場合、Jestではrejectsマッチャーを使用します。

If you expect a promise to be rejected, use the .rejects matcher. It works analogically to the .resolves matcher. If the promise is fulfilled, the test will automatically fail.

検証

次の関数で例外がスローされていることをテストしたいとします。

func.ts

export const syncFunc = () => {
  subFunc();
  throw new Error('syncFunc failed.');
};

export const asyncFunc = async () => {
  subFunc();
  throw new Error('asyncFunc failed.');
};

export const subFunc = () => {
  console.log('hoge');
};

正しいパターン

まず、冒頭で示した、正しいパターンの記述によりテストが正常にPASSするパターンです。

func.test.ts

import { syncFunc, asyncFunc, subFunc } from '../func';

jest.setTimeout(10000);

test('_syncFunc', () => {
  (subFunc as jest.Mock) = jest.fn().mockReturnValue(void 0);

  //同期関数のテスト - 正しいパターン
  expect(() => syncFunc()).toThrow(new Error('syncFunc failed.'));

  expect(subFunc).toBeCalledTimes(1);
});

test('_asyncFunc', async () => {
  (subFunc as jest.Mock) = jest.fn().mockReturnValue(void 0);

  //非同期関数のテスト - 正しいパターン
  await expect(asyncFunc()).rejects.toThrow(new Error('asyncFunc failed.'));

  expect(subFunc).toBeCalledTimes(1);
});

いずれのテストもPASSします。

$ npx jest              
 PASS  test/func.test.ts (5.387 s)
  ✓ _syncFunc (5 ms)
  ✓ _asyncFunc (3002 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        5.444 s
Ran all test suites.

正しくないパターン

次に、よくやりがちな、正しくないパターンの記述によりテストがFAILするパターンです。

test/func.test.ts

import { syncFunc, asyncFunc, subFunc } from '../func';

jest.setTimeout(10000);

test('_syncFunc', () => {
  (subFunc as jest.Mock) = jest.fn().mockReturnValue(void 0);

  //同期関数のテスト - 正くないパターン(無名関数となっていない)
  expect(syncFunc()).toThrow(new Error('syncFunc failed.'));

  expect(subFunc).toBeCalledTimes(1);
});

test('_asyncFunc', async () => {
  (subFunc as jest.Mock) = jest.fn().mockReturnValue(void 0);

  //非同期関数のテスト - 正しくないパターン(expectが同期呼び出しされていない)
  expect(asyncFunc()).rejects.toThrow(new Error('asyncFunc failed.'));

  expect(subFunc).toBeCalledTimes(1);
});

同期関数のテストを実行すると失敗しました。syncFuncがスローした例外がマッチャー側でキャッチできず関数の実行自体がFAILしています。

$ npx jest -t "_syncFunc"
 FAIL  test/func.test.ts
  ✕ _syncFunc (1 ms)
  ○ skipped _asyncFunc

  ● _syncFunc

    syncFunc failed.

      1 | export const syncFunc = () => {
      2 |   subFunc();
    > 3 |   throw new Error('syncFunc failed.');
        |         ^
      4 | };
      5 |
      6 | export const asyncFunc = async () => {

      at syncFunc (func.ts:3:9)
      at Object.<anonymous> (test/func.test.ts:9:18)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 skipped, 2 total
Snapshots:   0 total
Time:        2.248 s, estimated 3 s
Ran all test suites with tests matching "_syncFunc".

非同期関数のテストの実行も失敗しました。asyncFuncの同期呼び出しが完了する前にsubFuncの呼び出しが評価されており、テストがFAILしています。また非同期呼び出しされた関数の実行が完了する前にJestのテスト実行が終了しているため、Jest did not exit one second after the test run has completed.というWarningが出ています。

$ npx jest -t "_asyncFunc" 
 FAIL  test/func.test.ts
  ✕ _asyncFunc (2 ms)
  ○ skipped _syncFunc

  ● _asyncFunc

    expect(jest.fn()).toBeCalledTimes(expected)

    Expected number of calls: 1
    Received number of calls: 0

      18 |   expect(asyncFunc()).rejects.toThrow(new Error('asyncFunc failed.'));
      19 |
    > 20 |   expect(subFunc).toBeCalledTimes(1);
         |                   ^
      21 | });
      22 |

      at Object.<anonymous> (test/func.test.ts:20:19)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 skipped, 2 total
Snapshots:   0 total
Time:        2.182 s, estimated 3 s
Ran all test suites with tests matching "_asyncFunc".
Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.

参考

以上