JestのtoThrowの実験(テスト対象が同期か非同期かでの違い)
やりたいこと
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$/);
});
のように書きます。上のコードは通りません。
さいごに
書きながらも、ミスが起こりやすいだろうなと思って書いてました。ミスをなくすため(品質を担保するため)に書いているのに、ミスが誘発されているような…
もしも、不明点があったり、記事内に誤りがあれば、ご指摘いただけると幸いです。
-
await / MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await (2022-03-15閲覧) ↩︎
-
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