最近、ユニットテストを書いていて、これ大事な事だよなーと思うことについて書きたいと思います。
ユニットテストを書く時だけではなく、レビュー時の観点としても大事な事だと思うので、ぜひ参考にして頂ければうれしいです。
ユニットテストとは?
ユニットテストとは、作ったアプリケーションの部品に対するテストのことです。対象は、関数だったり、モジュールだったりエンティティだったり、ともかくアプリケーション内にある様々な部品に対するテストです。
ユニットテストは素早く実行でき、インターネット通信やファイル入出力の部分はモック化する必要があります。また、単体で実行できるので、通信環境は不要です。
ユニットテストとは逆に、結合テスト(インテグレーションテスト)というテストもあります。
ユニットテストと異なる部分は「通信を介してのテスト」、「ファイルの入出力を伴うテスト」などでしょうか。例えばフロントエンドのアプリケーションがBFF(Backend For Frontend)との通信することを含めてテストするときは結合テストと呼ばれるでしょう。
ユニットテストを書くときに大事な事とは?
ユニットテストで大事なことは以下だと思います。
- 何をテストするかを明確にする
- テストを実行した後の期待値を明確にする
- 混ぜるな危険、複数のテストケース
- 本当に必要なケースだけが見れるようにする
- テスト結果に変数は使わない
- テストは冗長で良い
- パラメタライズドテストとは?
- パラメタライズドテストの罠?
- 共通化しすぎない
- テスト結果が分かりやすいかどうかはちゃんと確認しよう
- 境界値はモック化する(インターネット通信を行わない)
- 境界値はモック化する(ファイル入出力は行わない)
何をテストするかを明確にする
ユニットテストではなく、手動のテストでは、以下のことを考えると思います。
- どの機能をテストするか(関数、クラス、画面)
- どんな観点でテストするか(要件を満たしているかのテスト、境界値付近のテスト、条件分岐のテスト)
- どんなデータを用意するか(変数、CSV、json)
- どのようにテストするか
- モックを使うか使わないか(単純なユーティリティ関数であれば、特にモックは不要)
- 期待値は何か
- どうやって正しいことを検証するか(関数の戻り値をチェックする、モック変数の中身をチェックする)
- テスト結果をどうやって見せるか(HTMLで出力、画像を残す、コンソールに出力を見せる)
ユニットテストでも同様に考えるのが良いと思います。場当たり的にテストコードを書くのではなく、「自分は何に対して、どのようなテストをするか」を考えるのが良いテストを書き続けられる大事な観点だと思います。(考えなしなテストは、レビューで「なぜこのテストをするのですか?」とか言われちゃいますよねw)
では、1つ1つ見ていきましょう。この記事で全部は書ききれないので、いくつかの記事に分けて書きたいと思います。
何をテストするかを明確にする
では、大事な事の1つ「何をテストするかを明確にする」です。
まずは、このテストコードの実行結果を見てみましょう。何のテストをしているか分かりますか?
PASS src/unit-test/what-do-you-want-to-test.spec.ts
√ plus function test 1 (1 ms)
テスト実行結果には「plus function test 1」と書かれているだけで、実行してみるとテストは成功しますが、何をやっているかは、コードをよく見ないと分かりません。コードは以下です。単純な足し算ですね。
// 0以上の数値を足して、その答えを返す関数
function plus(a: number, b: number) {
if (a < 0 || b < 0) {
throw Error("a or b is less than 0");
}
return a + b;
}
test("plus function test 1", () => {
// data
const a = 1;
const b = 1;
// exec
const result = plus(a, b);
// assert
expect(result).toBe(2);
});
もし、このテストが失敗していたり、このテストに関係するコードを修正するときに、1つ1つ理解しながら修正する必要があるため、時間がかかってしまいます。例のテストコードは非常に単純な関数なので、すぐに理解はできると思いますが、実際の開発ではそう簡単にはいきません。
最悪の場合、「うーん、面倒だから適当に修正してテストOKにしておくか」や「うーん、ちょっと時間がかかりそうだから、いったんこのテストコードはスキップしておこう」といった結果になってしまいます。そうなるとテストの漏れが発生し、デグレが発生し、本番で障害が発生し、ユーザに迷惑が掛かってしまうことになります。
テストコードは読みやすいように書いた方が良いです。なぜなら、テストコードを読んでいる時間も開発の時間だからです。テストコードを読むのに時間がかかると、本来の機能を実現するための時間を削ってしまうことになりかねません。テストコードも読みやすく、理解しやすいコードにしておくのが良いと思います。
それでは、ちょっとtestを書き換えてみましょう。これはどうでしょうか?
test("1 + 1 = 2", () => {
// data
const a = 1;
const b = 1;
// exec
const result = plus(a, b);
// assert
expect(result).toBe(2);
});
どのようなテストを行い、どのような期待値なのか分かるようになりましたね。1つのテストコードだけでは、「そんなの読んだ方が早いでしょ!」と思わるかもしれませんが、実際はコードを読む前にテストコードの実行結果一覧を見ると思います。大量にテスト結果があった場合はどうでしょう?そんなときに「どんなテストケースで失敗したのだろうか?」と結果を見ただけで分かるようにすることはとても大事です。
もちろん失敗したケースだけではなく、「不足しているケースはないだろうか?」と考える時にも有用です。テスト実行結果が、テスト内容を分かりやすく教えてくれたらどうでしょう?「あれ、このケースが抜けているから分岐を網羅するために、別なテストケースも追加しよう」とテスト改善につながることもあると思います。
ちなみに、テストフレームワークにもよりますが、テストは階層構造にすることもできますので、以下のように、「plus関数のテストであること」が分かるようにすることもできます。
// 0以上の数値を足して、その答えを返す関数
function plus(a: number, b: number) {
if (a < 0 || b < 0) {
throw Error("a or b is less than 0");
}
return a + b;
}
describe("plus function", () => {
test("1 + 1 = 2", () => {
// data
const a = 1;
const b = 1;
// exec
const result = plus(a, b);
// assert
expect(result).toBe(2);
});
test("1 + 2 = 3", () => {
// data
const a = 1;
const b = 2;
// exec
const result = plus(a, b);
// assert
expect(result).toBe(3);
});
});
テスト結果はこのような感じになります。階層構造でテスト実行結果が表示されるので、かなり見やすくなりましたね。
PASS src/unit-test/what-do-you-want-to-test.spec.ts
plus function
√ 1 + 1 = 2 (2 ms)
√ 1 + 2 = 3 (1 ms)
最後に
テストコードを書く事は人によっては「やる気の起きない」、「飽きてしまう作業」になりがちかなと思います。でも、テストを行うことは、素早くバグを見つけ、修正することでユーザ影響がある問題を未然に防ぐことにつながります。コード量が増えていくと、すべてのデグレチェックをマニュアルで行うことは不可能です。頻繁に修正・リリースを行うサービスの場合、より良いサービスを作り出すためにテストコードを書くことが必要不可欠です。となれば、本番コードと同様に、テストコードも理解しやすく・修正しやすいコードに維持する必要があると思います。
テストコードは奥が深いです。学べば学ぶほど力になると思うので、ぜひテストコードについてももっと学んでいきましょう!