多くのプロジェクトで、ユニットテストはCIのカバレッジチェックを通過するために最低限書くものになっている。急いでいくつかのテストケースを書き、パイプラインが緑になればマージする。しかし、形だけのために書いたテストと、その価値を理解して書いたテストはまったく別物であり、その差はプロジェクトが大きくなるにつれて明らかになる。
ユニットテストとは何か
ユニットテストは、切り出せる最小単位のコード、通常は関数やメソッドを、システムの他の部分から切り離した状態で検証する。目的はコードが「動く」ことを証明することではなく、通常のケース、境界値のケース、エラーのケースといった入力の種類ごとに、期待どおりに振る舞うことを確認することだ。
形だけのテストの兆候
形だけのテストは見分けやすい。「ハッピーパス」(きれいな入力と正しい結果)しか検証していない。結果を実際に確認するアサーションがなく、カバレッジを上げるために関数を呼んでいるだけ。モックを使いすぎて、テストがモック自体を検証しているだけになっている。あるいはtest1やtestFunctionのような曖昧な名前で、何を守るテストなのか誰にもわからない。
こうしたテストで得た高いカバレッジは、偽りの安心感を生む。数字は90%でも、重要な振る舞いを確認するテストがないため、実際のバグはすり抜けていく。
ユニットテストの本当の価値
ユニットテストの最大の価値は、書いた瞬間にバグを見つけることではなく、未来にある。第一に、テストがあれば自信を持ってリファクタリングできる。コードの構造を変えたとき、意図しない振る舞いの変化があればすぐにテストが知らせてくれる。テストがなければ、リファクタリングのたびに賭けをすることになる。第二に、テストは生きたドキュメントだ。関数のテストを読めば、コメントを読むより早く使い方がわかることが多い。しかもテストは古くならない。古くなれば失敗するからだ。第三に、テストは修正コストが低いうちに、できるだけ早くバグを見つけ、ステージングや本番環境までバグが到達するのを防ぐ。
あまり語られない利点もある。テストを書きにくいコードは、たいてい設計が良くないサインだ。たとえば一つの関数が多くのことをやりすぎていたり、多くのコンポーネントに強く依存していたりする。テストを書くことで、自分の設計を見直すきっかけになる。
意味のあるテストを書くには
テストに本当の価値を持たせるための原則はシンプルだ。テスト名は検証する振る舞いを表すこと(testCartではなくreturns_zero_when_cart_is_emptyのように)。一つのテストでは一つの振る舞いだけを検証すること。ハッピーパスだけでなく、境界値とエラーのケースを必ず含めること。モックするのはデータベース、API、システム時刻など本当の外部依存だけにし、検証したいロジック自体はモックしないこと。そして、誰もが頻繁に実行したくなるよう、テストを高速に保つこと。
義務として書かされると、テストは単なる手続きになる。必要性を理解すれば、テストは自分の仕事を速くし、デプロイのたびに安心して眠れるようにしてくれる道具になる。