これは ビットバンク株式会社 Advent Calendar 2020 の 1 日目の記事です。
はじめに
こんにちは、普段はサーバサイドでエンジニアしている monja です。
今日はそんな日常で使っている、弊 blog でも激オシの jest でちょっと悩んだ話をします。
なおこの記事で使用した各モジュールのバージョンは以下の通りです:
@types/jest 26.0.15
jest 26.6.3
ts-jest 26.4.4
typescript 3.9.7
不思議なFAILに遭遇
ある日の monja「よーし今日はここを修正するぞー」
// somecode.ts
export async function halfAmount(amount: number): Promise<number> {
// await some promise...
await new Promise(resolve => setTimeout(resolve, 100));
if (9000 < amount) {
throw "it's over 9000!";
}
return amount / 2;
}
// somecode.spec.ts
import { halfAmount } from './somecode';
describe('halfAmount', () => {
it('should return half of an argument', () => {
expect(halfAmount(5)).toBe(2.5);
});
});
※登場するコードは架空であり、実在するプロダクトとは何ら関係ありません
monja「仕様変更で amount が 9000 でなく 10000 まで指定できるようになったので、実装をこうして〜…」
@@ -3,8 +3,8 @@
// await some promise...
await new Promise((resolve) => setTimeout(resolve, 100));
- if(9000 < amount) {
- throw "it's over 9000!";
+ if(10000 < amount) {
+ throw "it's over 10000!";
}
return amount / 2;
「…おや? ここ異常系のテストないじゃん! 追加しよ〜」
@@ -4,4 +4,7 @@
it('should return half of an argument', async () => {
await expect(halfAmount(5)).resolves.toBe(2.5);
});
+ it('should throw if an argument is over 10000', async () => {
+ await expect(halfAmount(12345)).rejects.toThrow("it's over 10000!");
+ });
});
「というわけで jest るぞー」
$ yarn jest
...
FAIL .../somecode.spec.ts
...
● halfAmount › should throw if an argument is over 10000
expect(received).rejects.toThrow(expected)
Expected substring: "it's over 10000!"
Received function did not throw
5 | });
6 | it('should throw if an argument is over 10000', async () => {
> 7 | await expect(halfAmount(12345)).rejects.toThrow("it's over 10000!");
| ^
8 | });
9 | });
10 |
...
おや? テストがコケましたね。"Received function did not throw" って、throwしてるのに? なんでだろう…?
原因はなんだろう
他の所ではちゃんと非同期な reject を toThrow でマッチできていたのに。
そう思って
oO( toThrow にメッセージ指定しなければ pass するけどエラー判別したい所でつらいなぁ… )
とか
oO( 同期的なコードなら toThrow うまくいくのになぁ… )
などと試行錯誤しましたが、どうもうまくいかない。
そこでおもむろに jest の toThrow マッチャのソース を覗いてみました。
const toThrowExpectedString = (
...
): SyncExpectationResult => {
const pass = thrown !== null && thrown.message.includes(expected);
...
あっ、 thrown .message を見ていますね!
ここで実装の throw しているところを思い出してみましょう:
if (10000 < amount) {
throw "it's over 10000!";
}
たしかに、今回修正した実装では Error ではなく string を throw していますね!!
string には message というプロパティは無いので、マッチしないのはある意味あたりまえですね。
修正してみる
ここでテストコードを expect(...).rejects.toBe("it's over 10000!")
のように修正することもできますが、
string を throw するよりは Error を throw したほうがコールスタックの情報も残るので、今回は実装の throw の方を直してみましょう:
@@ -4,7 +4,7 @@
await new Promise((resolve) => setTimeout(resolve, 100));
if(10000 < amount) {
- throw "it's over 10000!";
+ throw new Error("it's over 10000!");
}
return amount / 2;
ということで、もいちど jest をエイヤ!
$ yarn jest
...
PASS .../somecode.spec.ts
halfAmount
✓ should return half of an argument (105 ms)
✓ should throw if an argument is over 10000 (107 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
...
無事テストが正しく通るようになりましたね!
……toThrowとは…… 🤔
まとめ
rejects.toThrow('...')
がうまくマッチしない時は、テストコードもそうだけど、そもそもどんな値が throw(reject)されているか見てみよう!
以上、monja がお送りしましたー。