ハッピーパスではなく境界・インバリアント・障害モードを狙って高シグナルなテストを生む、Claude Code向けのテスト生成プロンプトを学ぶ。

自動生成されたテストスイートは見た目には豪華に見えます: 数十のテスト、膨大なセットアップコード、あらゆる関数名がどこかに登場します。しかし多くのテストは「普通に動けばOK」というチェックに過ぎません。簡単に通り、バグをほとんど検出せず、読むにも保守するにも時間がかかります。
典型的なClaude Codeのテスト生成プロンプトでは、モデルは与えられた例入力を写す傾向があります。見た目が違うだけのバリエーションが増え、同じ振る舞いだけをカバーする結果になります。結果として、重要な箇所の網羅が薄い大きなスイートが出来上がります。
高シグナルなテストは違います。少数でも先月のインシデントを検出していたであろうテストです。危険な方法で振る舞いが変わると失敗し、無害なリファクタでは安定します。高シグナルなテスト1件は「期待した値を返す」チェック20件に相当する場合があります。
低価値なハッピーパス生成にはいくつかの明確な兆候があります:
例えば割引コードを適用する関数を考えてみてください。ハッピーパスのテストは「SAVE10で価格が下がる」ことを確認します。本当のバグは別の場所に潜みます: 0や負の価格、期限切れコード、丸めの端、最大割引上限など。これらが合計金額を壊し、顧客の怒りや深夜のロールバックを引き起こします。
目標は「より多くのテスト」から「より良いテスト」へ移ることです。焦点は3つ: 境界、障害モード、インバリアント。
高シグナルなユニットテストを得たいなら、「テストをもっとくれ」と要求するのをやめ、代わりに3種類のテストを要求してください。これが、Claude Codeのテスト生成プロンプトで有用なカバレッジを生むコアです。
境界はコードが受け入れる/生成する範囲の端です。多くの実際の欠陥はオフバイワン、空状態、タイムアウト問題で、ハッピーパスでは表に出ません。
最小値と最大値(0、1、最大長)、空か存在か(""、[]、nil)、オフバイワン(n-1、n、n+1)、時間制限(境界付近)を考えてください。
例: APIが「最大100アイテム」を受け入れるなら、3だけでなく100と101をテストします。
障害モードはシステムの壊れ方です: 不正な入力、依存の欠落、部分結果、上流のエラーなど。良い障害モードテストはストレス下での振る舞いを確認し、理想条件での出力だけをチェックしません。
例: データベース呼び出しが失敗したとき、関数は明確なエラーを返し、部分的な書き込みを避けるか?
インバリアントは呼び出し前後で常に真であるべき事実です。あいまいな正しさを明確なアサーションに変えます。
例:
これら3つを狙えばテストは少なくなりますが、それぞれがより多くのシグナルを持ちます。
早すぎる段階でテストを要求すると、丁寧な「期待通り動く」チェックの山が返ってきます。簡単な対策は、まず小さな契約を書き、それからその契約からテストを生成することです。これがClaude Codeのテスト生成プロンプトを実際にバグを見つけるものに変える最速の方法です。
読み切れる長さに収めた契約を作りましょう。目安は5〜10行で、次の3つに答えます: 入力は何か、出力は何か、他に何が変わるか。
契約はコードではなくプレーンな言葉で、テスト可能なことだけを含めてください。
それができたら、現実がどこで仮定を壊すかをスキャンしてください。そこが境界(最小/最大、ゼロ、オーバーフロー、空文字、重複)や障害モード(タイムアウト、権限拒否、一意制約違反、破損入力)になります。
具体例: reserveInventory(itemId, qty) のような機能を考えると、契約は qty が正の整数であること、関数はアトミックであること、在庫が負にならないことを含めるかもしれません。すると高シグナルテストはすぐに明らかになります: qty = 0、qty = 1、在庫より大きいqty、同時呼び出し、途中で強制的にDBエラーが発生した場合など。
Koder.aiのようなチャットベースのツールを使っている場合でも、ワークフローは同じです: まずチャットで契約を書き、その後で境界・障害・「決して起きてはならない」リストを直接攻撃するテストを生成します。
少ないテストでそれぞれが重みを持つセットを欲しいときは、このClaude Codeテスト生成プロンプトを使ってください。重要な一手はまずテスト計画を強制し、計画を承認してからテストコードを生成することです。
You are helping me write HIGH-SIGNAL unit tests.
Context
- Language/framework: <fill in>
- Function/module under test: <name + short description>
- Inputs: <types, ranges, constraints>
- Outputs: <types + meaning>
- Side effects/external calls: <db, network, clock, randomness>
Contract (keep it small)
1) Preconditions: <what must be true>
2) Postconditions: <what must be true after>
3) Error behavior: <how failures are surfaced>
Task
PHASE 1 (plan only, no code):
A) Propose 6-10 tests max. Do not include “happy path” unless it protects an invariant.
B) For each test, state: intent, setup, input, expected result, and WHY it is high-signal.
C) Invariants: list 3-5 invariants and how each will be asserted.
D) Boundary matrix: propose a small matrix of boundary values (min/max/empty/null/off-by-one/too-long/invalid enum).
E) Failure modes: list negative tests that prove safe behavior (no crash, no partial write, clear error).
Stop after PHASE 1 and ask for approval.
PHASE 2 (after approval):
Generate the actual test code with clear names and minimal mocks.
実用的なコツとして、境界マトリクスをコンパクトな表にすることを要求すると、抜けが明らかになります:
| Dimension | Valid edge | Just outside | “Weird” value | Expected behavior |
|---|---|---|---|---|
| length | 0 | -1 | 10,000 | error vs clamp vs accept |
Claudeが20件のテストを提案したら突き返してください。類似ケースを統合して、本当にバグを見つけるものだけを残すように促します。
まず挙動の小さな具体的な契約で始めます。関数シグネチャ、入力と出力の短い説明、既存テスト(ハッピーパスだけでも)を貼り付けてください。これによりモデルはコードが実際に何をするかに基づいて出力し、推測に走りにくくなります。
次に、テストコードを要求する前にリスクテーブルを要求します。列は3つ必須: 境界ケース(有効入力の端)、障害モード(悪い入力、欠落データ、タイムアウト)、インバリアント(常に成り立つべきルール)。各行に「なぜこれが壊れるか」を1文付けてください。シンプルな表は多数のテストファイルよりもギャップを明らかにします。
その後、各テストが固有のバグ検出目的を持つ最小セットを選びます。2つのテストが同じ理由で失敗するなら、強い方だけを残します。
実用的な選択ルール:
最後に、各テストに「失敗したらどんなバグを検出したか」を短く書くことを要求してください。その説明が漠然としている(「振る舞いを検証」)なら、そのテストはおそらく低シグナルです。
インバリアントは有効な入力であれば常に成り立つべきルールです。まずそれをプレーンな文に書き、その後にテストで証明できるアサーションに変換します。
守るべきインバリアントは通常、安全性(データ損失しない)、一貫性(同じ入力は同じ出力)、上限(上限を超えない)に関するものが良いです。
インバリアントを短い文に書き、それからテストで観測できる証拠(返り値、保存されたデータ、発行されたイベント、依存への呼び出し)を決めます。強いアサーションは結果と副作用の両方をチェックします。多くのバグは「OKを返したが、書き込みが間違っていた」ケースに隠れます。
例えばクーポンを注文に適用する関数があるとします:
これを具体的なアサーションに変えると:
expect(result.total).toBeGreaterThanOrEqual(0)
expect(db.getOrder(orderId).discountCents).toBe(originalDiscountCents)
「期待通りの結果を返す」などの漠然としたアサーションは避け、具体的なルール(非負)や副作用(割引が一度だけ適用されている)を確認してください。
各インバリアントに対して、テスト内にそのインバリアントを破るデータの短い例をコメントとして書いておくとよいです。これによりテストが後でハッピーパスへと退化するのを防げます。
維持しやすいパターン:
高シグナルなテストはコードが安全に失敗することを確認するものです。モデルがハッピーパスしか書かない場合、依存関係や入力が乱れたときの挙動は分かりません。
まずその機能にとって「安全に失敗する」とは何かを定義してください。型付きエラーを返すのか、デフォルトにフォールバックするのか、一度だけ再試行して止めるのか。期待される振る舞いを一文で書き、それをテストで証明します。
Claude Codeに障害モードテストを求めるときは、目的を厳格にしてください: システムが壊れる方法を網羅し、期待する正確な応答をアサートさせます。便利な一文は: 「浅いテストの多数より、強いアサーションを持つ少数のテストを優先する」です。
良い障害カテゴリ:
例: ユーザを作成しウェルカムメールサービスを呼ぶエンドポイントがあるとします。低価値なテストは「201が返る」をチェックするだけですが、高シグナルな障害テストはメールサービスがタイムアウトしたらユーザを作るか(201とemail_pendingフラグ)、あるいはユーザを作らずに503を返すか、どちらかの一貫した挙動をアサートします。応答と副作用の両方をチェックしてください。
また、何を漏らさないかもテストしてください。バリデーションが失敗したらDBに何も書かれないこと、依存が破損ペイロードを返しても未処理例外や生のスタックトレースを返さないことなどです。
低価値なテストセットはモデルが量で報酬を得ると発生します。「20ユニットテストを出して」と要求すると、同じような小さな変化ばかりが増え、本質的なことをほとんど捉えません。
一般的な罠:
例: 「ユーザ作成」関数。10のハッピーパステストはメール文字列を変えるだけで、重複メール拒否、空のパスワード処理、返されるユーザIDの一意性など重要な点を見逃します。
レビュでのガードレール:
想定機能: チェックアウトでクーポンコードを適用する。
契約(小さくテスト可能に): カートの小計(セント)とオプションのクーポンを受け取り、最終合計(セント)を返す。ルール: パーセントクーポンは最も近いセントに切り捨て、固定クーポンは固定額を差し引き、合計は0より小さくならない。クーポンは無効・期限切れ・既使用の可能性がある。
「applyCoupon()のテストを作って」と頼むのではなく、この契約に紐づいた境界、障害モード、インバリアントを要求してください。
数学や検証を壊しやすい入力を選びます: 空のクーポン文字列、小計=0、最低購入額の直前と直後、固定割引が小計を上回るケース、33%のような丸めを生むパーセント。
クーポン検索が失敗したり状態が間違っていることを想定します: クーポンサービスがダウン、クーポンが期限切れ、既にこのユーザが使っている。テストはその後に何が起きるか(クーポン拒否と明確なエラー、合計は変わらない等)を証明します。
最小で高シグナルなテストセット(5件)とそれぞれが捕らえるもの:
これらが通れば、重複したハッピーパステストでスイートを埋めることなく、一般的な破綻点をカバーできます。
生成物を受け入れる前に簡易品質チェックを行ってください。目的は、それぞれのテストが特定で起こり得るバグからあなたを守ることです。
このチェックリストを合格ゲートに使います:
生成後の実用的なコツ: テスト名を「should <振る舞い> when <境界条件>」や「should not <悪い結果> when <障害>」にリネームしてください。きれいにリネームできないなら、それは焦点が甘い証拠です。
Koder.aiで開発するなら、このサイクルはスナップショットとロールバックと相性が良い: テストを生成して実行し、ノイズが増えたらロールバックして高シグナルセットを安定させます。
このプロンプトを一回限りのものとせず、再利用可能なハーネスとして扱ってください。境界、障害モード、インバリアントを強制する青写真プロンプトを保存し、関数やエンドポイント、UIフローごとに使い回します。
すぐに結果を上げる小さな習慣: 各テストについて「このテストが失敗したらどのバグを捕らえるか」を1文で書かせること。もしその文が一般的なら、そのテストはノイズです。
ドメインのインバリアント一覧を持っておくこと。頭の中に入れずに記録しましょう。実際のバグを見つけるたびに追加してください。
繰り返し可能な軽量ワークフロー:
チャットでアプリを構築する場合は、契約、計画、生成されたテストが同じ場所にあるようKoder.ai(koder.ai)内でこのサイクルを回してください。リファクタで不意に振る舞いが変わったらスナップショットとロールバックで比較し、安定するまで反復します。
デフォルト: 実際のバグを見つけられる小さなセットを目標にしてください。
手早く決めるなら 関数/モジュールあたり6–10件 が実用的です。もっと必要なら、そのユニットがやりすぎているか契約が不明確なことが多いです。
ハッピーパスのテストは主に「例がまだ動く」ことを確認するだけで、本番で壊れる原因を見逃しがちです。
高シグナルテストは次を狙います:
まずは一息で読める小さな契約を書いてください:
その契約からテストを生成してください。単なる例だけから生成するとハッピーパスばかりになります。
まずはこれを優先してテストすべきです:
入力の各次元につき1〜2件選び、各テストが固有のリスクをカバーするようにします。
良い障害モードテストは2つを証明します:
DB書き込みが関与するなら、失敗後にストレージで何が起きたかを必ずチェックしてください。
基本的な方法: インバリアントを観測可能な結果に変えてアサーションを書く。
例:
expect(total).toBeGreaterThanOrEqual(0)返り値と副作用の両方をチェックする方が強固です。多くのバグは「OKが返ったが書き込みが間違っている」ケースで起きます。
ハッピーパステストは、インバリアントを守る場合や重要な統合を固定化する場合に価値があります。
残すべき良い理由:
それ以外は、境界/障害のテストに置き換える方が効果的です。
まずはPHASE 1: 計画のみを要求してください。
モデルに次を出力させます:
計画を承認してからPHASE 2でテストコードを生成させると、20件の似たようなテストが出るのを防げます。
モックを最小限に保ち、実際に制御できる境界だけをモックしてください。
オーバーモックを避けるための指針:
リファクタで振る舞いが変わらないのにテストが壊れるなら、それは過度なモックか実装依存になっていることが多いです。
簡単な削除テストを行ってください:
重複を探す: