[Jest] throw Matcherで例外処理のテストを実装してみた

2022.05.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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

今回は、JavaScript Testing FrameworkであるJestで、例外処理のテストを実装してみました。

やってみた

Jestでは、.toThrow()というMactherが用意されており、これを使用して例外処理の評価を行うことができます。

toThrowError().toThrow()のエイリアスです。クラス、エラーメッセージ、インスタンスオブジェクトなどを評価対象として指定できます。

example

function drinkFlavor(flavor) {
  if (flavor == 'octopus') {
    throw new DisgustingFlavorError('yuck, octopus flavor');
  }
  // Do some other stuff
}

test('throws on octopus', () => {
  function drinkOctopus() {
    drinkFlavor('octopus');
  }

  // Test that the error message says "yuck" somewhere: these are equivalent
  expect(drinkOctopus).toThrowError(/yuck/);
  expect(drinkOctopus).toThrowError('yuck');

  // Test the exact error message
  expect(drinkOctopus).toThrowError(/^yuck, octopus flavor$/);
  expect(drinkOctopus).toThrowError(new Error('yuck, octopus flavor'));

  // Test that we get a DisgustingFlavorError
  expect(drinkOctopus).toThrowError(DisgustingFlavorError);
});

同期関数、非同期関数それぞれで.toThrow()を実際に使ってみます。

同期関数の場合

まず、次のような同期関数(Sync Function)の場合。引数がhogeの場合にErrorをThrowします。

main.ts

export const main = (arg: string): string => {
  try {
    if (arg === 'hoge') {
      throw new Error();
    }
    return arg;
  } catch (e) {
    throw new Error(arg);
  }
};

テストを成功させてみる

まず成功パターンです。上記の同期関数を対象としたテストを.toThrow()で記述します。

test/main.test.ts

import { main } from '../main';

describe('main', () => {
  test('toThrow with class', () => {
    expect(() => main('hoge')).toThrow(Error);
  });

  test('toThrow with message', () => {
    expect(() => main('hoge')).toThrow('hoge');
  });

  test('toThrow with instance', () => {
    expect(() => main('hoge')).toThrow(new Error('hoge'));
  });
});

テストを実行すると、いずれのテストケースもパスさせられました。

npx jest main.test.ts 
 PASS  test/main.test.ts (7.023 s)
  main
    ✓ toThrow with class (4 ms)
    ✓ toThrow with message (1 ms)
    ✓ toThrow with instance

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        7.081 s
Ran all test suites matching /main.test.ts/i.

テストを失敗させてみる

次に失敗パターンです。.toThrow()の引数をそれぞれ先程と変更しています。

import { main } from '../main';

describe('main', () => {
  test('toThrow with class', () => {
    expect(() => main('hoge')).toThrow(TypeError);
  });

  test('toThrow with message', () => {
    expect(() => main('hoge')).toThrow('nyao');
  });

  test('toThrow with instance', () => {
    expect(() => main('hoge')).toThrow(new Error('fuga'));
  });
});

テストを実行すると、いずれのテストケースもフェイルさせられました。

$ npx jest main.test.ts
 FAIL  test/main.test.ts (5.774 s)
  main
    ✕ toThrow with class (16 ms)
    ✕ toThrow with message (2 ms)
    ✕ toThrow with instance (3 ms)

  ● main › toThrow with class

    expect(received).toThrow(expected)

    Expected constructor: TypeError
    Received constructor: Error

    Received message: "hoge"

           6 |     return arg;
           7 |   } catch (e) {
        >  8 |     throw new Error(arg);
             |           ^
           9 |   }
          10 | };
          11 |

          at Object.<anonymous>.exports.main (main.ts:8:11)
          at test/main.test.ts:5:18
          at Object.<anonymous> (node_modules/expect/build/toThrowMatchers.js:83:11)
          at Object.throwingMatcher [as toThrow] (node_modules/expect/build/index.js:338:21)
          at Object.<anonymous> (test/main.test.ts:5:32)

      3 | describe('main', () => {
      4 |   test('toThrow with class', () => {
    > 5 |     expect(() => main('hoge')).toThrow(TypeError);
        |                                ^
      6 |   });
      7 |
      8 |   test('toThrow with message', () => {

      at Object.<anonymous> (test/main.test.ts:5:32)

  ● main › toThrow with message

    expect(received).toThrow(expected)

    Expected substring: "nyao"
    Received message:   "hoge"

           6 |     return arg;
           7 |   } catch (e) {
        >  8 |     throw new Error(arg);
             |           ^
           9 |   }
          10 | };
          11 |

          at Object.<anonymous>.exports.main (main.ts:8:11)
          at test/main.test.ts:9:18
          at Object.<anonymous> (node_modules/expect/build/toThrowMatchers.js:83:11)
          at Object.throwingMatcher [as toThrow] (node_modules/expect/build/index.js:338:21)
          at Object.<anonymous> (test/main.test.ts:9:32)

       7 |
       8 |   test('toThrow with message', () => {
    >  9 |     expect(() => main('hoge')).toThrow('nyao');
         |                                ^
      10 |   });
      11 |
      12 |   test('toThrow with instance', () => {

      at Object.<anonymous> (test/main.test.ts:9:32)

  ● main › toThrow with instance

    expect(received).toThrow(expected)

    Expected message: "fuga"
    Received message: "hoge"

           6 |     return arg;
           7 |   } catch (e) {
        >  8 |     throw new Error(arg);
             |           ^
           9 |   }
          10 | };
          11 |

          at Object.<anonymous>.exports.main (main.ts:8:11)
          at test/main.test.ts:13:18
          at Object.<anonymous> (node_modules/expect/build/toThrowMatchers.js:83:11)
          at Object.throwingMatcher [as toThrow] (node_modules/expect/build/index.js:338:21)
          at Object.<anonymous> (test/main.test.ts:13:32)

      11 |
      12 |   test('toThrow with instance', () => {
    > 13 |     expect(() => main('hoge')).toThrow(new Error('fuga'));
         |                                ^
      14 |   });
      15 | });
      16 |

      at Object.<anonymous> (test/main.test.ts:13:32)

Test Suites: 1 failed, 1 total
Tests:       3 failed, 3 total
Snapshots:   0 total
Time:        5.834 s, estimated 6 s
Ran all test suites matching /main.test.ts/i.

非同期関数の場合

続いて、次のような非同期関数(Async Function)の場合。

async_main.ts

export const main = async (arg: string): Promise<string> => {
  try {
    if (arg === 'hoge') {
      throw new Error();
    }
    return arg;
  } catch (e) {
    throw new Error(arg);
  }
};

非同期関数の場合も使い方は同じとなりますが、Promiseを解決できるように.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.

test/async_main.test.ts

import { main } from '../async_main';

describe('main', () => {
  test('Error toThrow', () => {
    expect(() => main('hoge')).rejects.toThrow(Error);
  });
});

テストを実行すると、パスさせられました。

$ npx jest async_main.test.ts
 PASS  test/async_main.test.ts
  main
    ✓ Error toThrow (3 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.845 s, estimated 6 s
Ran all test suites matching /async_main.test.ts/i.

おわりに

JavaScript Testing FrameworkであるJestで、例外処理のテストをしてみました。

正常系のテストは実装するかと思いますが、エラー系のテストは面倒くさがって省いてしまうこともあると思います。しかし今回紹介したJestの.throwを使えば簡単に実装できるので活用していきたいですね。

以上