メインコンテンツへスキップ

概要

テストコードは、システムが意図どおりに振る舞うことを検証するコードであり、継続的な変更を可能にする安全網です。 テストコードで重要なのは、システムを守ることです。システムを守るテストとは、挙動が正しいときに通り、挙動が壊れたときにコケるテストのことです。その価値はテストコードが全て通ることではなく、本番環境にリリースされる前に意図しない挙動を防ぐことにあります。 本ページでは、そうしたテストを支える原則と、テストを保護的にする書き方を解説します。

原則

テストの目的はソフトウェアの成長を持続可能にすること

自動テストの目的は、ソフトウェアの成長を持続可能にすることです。変更の影響を信頼できる結果として短時間で確認できる状態を保つことで、チームは自信を持ってシステムを変更し続けられます。意図しない挙動の検知はそのための手段であり、それ自体が最終目的ではありません。 したがって、質とスピードはトレードオフではありません。テストを省いて得られる速さは一時的なもので、その代償は意図しない挙動の混入・手動での再確認・既存コードへ手を入れることへの恐れとして、すぐに返ってきます。テストを書く時間が無いのではなく、テストを書かないから時間が無いのです。 リファクタリングや段階的な改善には、変更後も既存の挙動が保たれているという確信が必要であり、テストコードがそれを与えます。 生成AIツールが変更を提案する時代には、この点はさらに重要になります。提案された変更を受け入れてよいかどうかに最終的に答えるのはテストコードであり、テストコードの品質がどれだけの作業を委任できるかを決めます。

通ることもコケることも信頼できる状態を保つ

テストコードがシステムを守れるのは、その結果を両方向で信頼できる間だけです。テストコードが信頼を損なう失敗には2つの種類があります。

偽陰性 — 壊れているのに通る

挙動が壊れているのにテストが通ることです。断片的な検証や検証対象のモックが原因で起こり、存在しない安全が報告されて、意図しない挙動が本番環境にリリースされてしまいます。

偽陽性 — 正しいのにコケる

挙動は正しいのにテストがコケることです。実装の詳細や不安定なタイミングへの依存が原因で起こり、頻発すると失敗が無視されるようになって、本当に挙動が壊れたときまで見逃されます。
検証を強化して偽陰性を減らし、実装の内部ではなく観察可能な挙動を検証して偽陽性を減らします。両方を低く保つことで、テストコードは信頼を獲得します。本ページの後半の検証パターンは、主に偽陰性を防ぐためのものです。

カバレッジは目的ではなく結果

カバレッジは、テストコードがどの行を実行したかを測るものであり、正しい挙動を検証しているかを測るものではありません。ある行は、それが正しく動くという検証なしに実行できてしまうため、高いカバレッジと「ほとんど何も守らないテスト」は両立してしまいます。 確実な順序は、まず絶対に壊れてはいけない挙動を決め、それが壊れたら検知できるテストケースを追加することです。カバレッジはその結果として上がります。

テストは実装と同じプルリクエストで書く

挙動の変更と、それを守るテストは同じプルリクエストに含めます。テストを後続の対応に先送りすると、その挙動が守られていない期間が生まれ、実際には後続の対応は新しい仕事と競合して後回しになりがちです。 テストコードのレビューは実装コードと同等以上に厳密に行います。弱い検証を今日承認することは、将来の意図しない挙動を見逃すことと同じです。 変更をレビューしやすい単位に保つ方法はプルリクエストを参照してください。

テストを保護的にする書き方

次のパターンは、テストが一見問題なさそうでも、本来捕まえるはずのバグを捕まえられないケース、すなわち偽陰性の原因です。いずれも、重要な壊れ方に対して確実にコケるところまでテストを強化します。 同じパターンは、生成AIツールが出力するテストコードに繰り返し現れる弱点でもあります。生成されたテストは保護ではなく「テストが通ること」に最適化されがちなので、マージ前に人が書いたテストと同じ厳しさで、これらのパターンに照らして見直します。

1. 実行されたかではなく、実行状態を検証する

コールバックが呼ばれたことだけを検証すると、3つのバグを隠します。1回でよいハンドラが2回発火するケース、誤った引数(別のユーザーのデータなど)で呼ばれるケース、そして本来は決して走らないはずのエラー経路のコールバックが走るケースです。回数と引数まで固定し、呼ばれないはずの処理が一度も呼ばれていないことを検証します。

2. 一部ではなく、期待値の全体を検証する

結果のうち1つのフィールドだけを確認すると、それ以外のフィールドはすべて無防備になります。検証していないフィールドの欠落・改名・破損は、静かに通過してしまいます。構造全体をリテラルの期待値と比較し、結果の形や内容への意図しない変更がテストを確実に失敗させるようにします。
生成されたテストでは特に、現在の挙動が通るのに必要な最小限の検証しか書かれないことがよくあります。今日はテストが通っても、検証されていない部分の挙動を壊す将来の拡張は保証されません。
ゆるいマッチャも同じように検証を弱めます。toBeTruthybe_truthyは空でない文字列・数値など、真と評価される任意の値でも通るため、リファクタリングがbooleanの返り値を別の型にこっそり変えても通り続けます。正確な値と型を検証します。

3. 検証対象のロジックではなく、外部境界をモックする

「テストが通ること」が目的になると、生成AIツールは検証対象のロジックそのものをモックしたり、期待値を実装が現在返す値に合わせて調整したりすることがあります。実装自体を答え合わせの正解として扱っているため、実装が誤っていてもテストは通ってしまいます。 モックの本来の役割はその逆で、現在時刻やネットワーク応答のような外部境界を固定し、出力を決定的にすることです。たとえば有効期限の日時を含むメール本文のように現在時刻に依存する出力は、時刻を固定しない限り断片的な検証しかできません。時刻をモックで固定すれば、パターン2のとおり期待値の全体を検証できます。 どのケースでモックをどう使うかをプロジェクトのカスタムインストラクションに記述し、期待値が実装の出力ではなく仕様に由来していることをレビューで確認します。

関連ページ

プルリクエスト

テストは、それが守る変更と同じレビュー単位で提出します。

Vibe Coding

信頼できるスイートの上で、実装を生成AIツールに委任します。