🧪

JestのtoThrowの実験(テスト対象が同期か非同期かでの違い)

2022/03/15に公開

やりたいこと

Jestを使って、テスト対象の関数が期待した例外を吐き出すか知りたい。

いつもややこしいと思っていること

toThrow()は、テスト対象が同期か非同期かで、書き方が違う。
いつも、テストを書く前に実験して「どっちだったっけ…🤔」となるので、備忘録として書いておく。
ちなみに、公式のリファレンスでもあるのだが、同期処理の場合はこちら、非同期処理の場合はこちら と同じページで「toThrow()」調べても出てこないのが、ちょっと優しくないなと思う。結論から言うと、非同期処理はrejectsを挟まないといけない(他にもあるんだけど、rejectsさえ覚えておけばGoogleで「rejects toThrow」と調べたら、欲しい情報が出てくる、「toThrow」だけだと出てこないかも...)。

テスト対象の関数(class)

いちいち、定義するのは面倒なので、適当なclassを使って呼び出します。こんな感じです。

describe("Throw test", () => {
  class Test {
    constructor() {
      console.log("コンストラクタが呼ばれたよ");
    }

    test = () => {
      throw new Error("test throw");
    };

    asyncTest = async () => {
      throw new Error("asyncTest throw");
    };
  }
  const test = new Test();
  it("ここにテストを適当に書く", () => {
    // ここにテストを適当に書く
  });
});

以下ではitしか書きませんので、ご了承ください。

ここで確認すること

  • throwされたかどうかのみをassertする
  • throwされたかどうかと、そのメッセージの部分一致をassertする
  • throwされたかどうかと、そのメッセージの完全一致をassertする

throwされたかどうかのみをassertする

同期型関数(同期処理)の場合

何のひねりもなく以下の通りです。

it("同期型関数、throwされたかどうか", () => {
    expect(() => {
      test.test();
    }).toThrow();
  });

非同期型関数(非同期処理)の場合

.toThrow()の前に.rejectsを入れなくてはいけません。そして、expectの中に、無名関数を入れると動きません。おそらく、Promiseのrejected[1]から来ているのではないかな、と勝手に思っています(もしも、詳しい方がいらっしゃったら、教えていただけると嬉しいです)。
そのため、以下のように書きます。

it("非同期型関数、throwされたかどうか", async () => {
  await expect(test.asyncTest()).rejects.toThrow();
});

throwされたかどうかと、そのメッセージの部分一致をassertする

このchapter(節)以降は上の原則が変わらず、正規表現が使えるよ!という話だけなので、正規表現が分かる方は飛ばしてもらっても大丈夫だと思います。
.toThrow()の引数に、例えば.toThrow("Hello")のような引数を入れると部分一致でassertしてくれます。この場合、例えばテスト対象の関数が吐くエラー文が"Hello world"だろうが"World Hello"だろうが、通って(passして)しまいます。そのため、より厳密にしたい場合は正規表現を使います[2]
例えば以下のような感じです。

前方一致の場合(同期、非同期)

以下のテストコードは通るはずです。

  it("同期型関数、throw、前方一致", () => {
    expect(() => {
      test.test();
    }).toThrow(/^test.*/);
  });

  it("非同期型関数、throw、前方一致", async () => {
    await expect(test.asyncTest()).rejects.toThrow(/^asyncTest.*/);
  });

  it("同期型関数、throw、部分一致", () => {
    expect(() => {
      test.test();
    }).toThrow("test");
  });

  it("非同期型関数、throw、部分一致", async () => {
    await expect(test.asyncTest()).rejects.toThrow("asyncTest");
  });

前方一致の場合、try{}catch(error){}を使って何かエラーが来たものを、呼び出し元に伝えるthrow new Error("Errorだよ\n" + error)と言った場合に便利かなと思います。

逆に、以下のテストコードは、テスト対象の関数が後方に"throw"を吐くにもかかわらず、前方一致で"throw"をassertしているので、通りません。

  it("同期型関数、throw、前方一致", () => {
    expect(() => {
      test.test();
    }).toThrow(/^throw.*/);
  });

  it("非同期型関数、throw、前方一致", async () => {
    await expect(test.asyncTest()).rejects.toThrow(/^throw.*/);
  });

完全一致の場合(同期、非同期)

こちらは、期待通りの動きをしてもらおうと思うと、正規表現で書くしかなさそうです。例えば"throw"だけを吐いて欲しい場合は、.toThrow("throw")では駄目です。

以下の通って欲しいテストコードは、通ります。

  it("同期型関数、throw、完全一致", () => {
    expect(() => {
      test.test();
    }).toThrow(/^test throw$/);
  });

  it("非同期型関数、throw、完全一致", async () => {
    await expect(test.asyncTest()).rejects.toThrow(/^asyncTest throw$/);
  });

逆に「"throw"の完全一致をみたい」場合、以下の通ってほしくないテストコードは、通ります。なぜならtoThrow("throw")は部分一致だから。

  it("同期型関数、throw、部分一致", () => {
    expect(() => {
      test.test();
    }).toThrow("throw");
  });

  it("非同期型関数、throw、部分一致", async () => {
    await expect(test.asyncTest()).rejects.toThrow("throw");
  });

これを、通さないようにするには(期待した挙動を得るには)

  it("同期型関数、throw、部分一致", () => {
    expect(() => {
      test.test();
    }).toThrow(/^throw$/);
  });

  it("非同期型関数、throw、部分一致", async () => {
    await expect(test.asyncTest()).rejects.toThrow(/^throw$/);
  });

のように書きます。上のコードは通りません。

さいごに

書きながらも、ミスが起こりやすいだろうなと思って書いてました。ミスをなくすため(品質を担保するため)に書いているのに、ミスが誘発されているような…
 もしも、不明点があったり、記事内に誤りがあれば、ご指摘いただけると幸いです。

脚注
  1. await / MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await (2022-03-15閲覧) ↩︎

  2. How can I check if a function throws an error which contains a text in Jest?
    / Sarun UK and jonrsharpe (Stackoverflow) https://stackoverflow.com/questions/64275525/how-can-i-check-if-a-function-throws-an-error-which-contains-a-text-in-jest (2022-03-15閲覧) ↩︎

Discussion