Claude Codeでテストから仕様を読む — 壊して裏を取る手順
仕様書が無いモジュールのテスト名を「入力→期待結果」の表にし、わざと壊して落ちるか確かめる手順。テストが仕様を表すのか空洞なのかを見分けます。
参加直後のリポジトリに仕様書が無いとき、いちばん近い代わりになるのはテストです。ただしテストは「書いてあること」しか語らず、「書いていないこと」は語りません。しかも、緑色のテストが仕様を守っているとは限りません。
この記事では、Claude Codeに対象モジュールのテストを読ませて「入力 → 期待結果」の表にし、その表の各行を実際に壊して確かめる流れを組みます。読む段階で終わらせず、テスト実行まで通して裏を取るのが要点です。テストが無いコードに書く側の話はClaude Codeで特性テストを書くにあります。ここで扱うのは、すでにあるテストを読む側です。
テスト名の一覧は仕様の下書きになる
テストの関数名は、書いた人が「この挙動を守りたい」と思った箇所の目録です。test_free_shipping_at_threshold や test_negative_quantity_raises のような名前が並んでいれば、中身を読む前に仕様の輪郭が見えます。
下書き止まりなのは、名前と中身がずれることがあるからです。名前は「境界値で送料無料」と言っているのに、実際の入力は境界から遠い値で、どの実装でも通ってしまう。この種のずれは、名前を読むだけでは見つかりません。そこで読む工程を3つに分けます。
テストから仕様を取り出す流れ
- 1
名前を読んで箇条書きにする
テスト名と、あればdocstringから、挙動を1行ずつ書き出します。
- 2
中身を読んで入力と期待値の表にする
各テストが実際に渡している入力と、検証している値を拾います。名前と食い違う行に印を付けます。
- 3
壊して落ちるか確かめる
表の行ごとに、その挙動を壊す変更を1つ入れてテストを回します。落ちれば仕様として機能しており、落ちなければ空洞です。
Claude Codeに読ませるプロンプト
起動したら、まず大きく掴ませるのが公式の入門のやり方です。common-workflowsの「Understand new codebases」は、全体の概要を聞き、アーキテクチャーのパターンやデータモデルへ絞っていく順を示し、プロジェクト固有の用語集を頼むことも勧めています。テストを読ませる前に、この用語集を取っておくと、期待値に出てくる業務用語の意味で迷いません。
そのうえで、対象を1モジュールに絞って読ませます。例として、送料計算の shipping/fee.py とそのテストを読む場面を想定します。
shipping/fee.py の仕様書は無い。tests/test_shipping_fee.py だけを読んで、
次の表を作って。コードの実装は読まないで。
列: テスト名 / 渡している入力 / 検証している期待結果 / 名前と中身が食い違う点
最後に、テストが触れていない入力の種類を箇条書きにして。「実装は読まないで」と言うのが肝です。実装を先に読ませると、Claudeは実装から仕様を逆算して表を埋め、テストが何を語っているのかが混ざります。読む対象を絞る指示は、best-practicesが示す「具体的に頼む」書き方と同じ方向です。公式の例では「fix the login bug」と曖昧に頼む代わりに、場所と再現テストまで指定しています。
返ってくる表は、例えば次のような形になります(例示であって、実際の出力ではありません)。
| テスト名 | 入力 | 期待結果 | 食い違い |
|---|---|---|---|
| test_free_shipping_at_threshold | 入力小計5000 | 期待結果送料0 | 食い違い名前は境界値だが入力が5000ちょうどか要確認 |
| test_fee_for_island | 入力小計3000、離島 | 期待結果送料1200 | 食い違いなし |
| test_negative_quantity_raises | 入力数量 -1 | 期待結果ValueError | 食い違いなし |
| test_coupon_applied | 入力クーポン付き | 期待結果送料0 | 食い違い割引の順序は未検証 |
「触れていない入力」の欄が、表の価値を決めます。例えば「小計0」「複数クーポン」「離島かつ送料無料」が並んだとしたら、それは仕様が決まっていないのではなく、テストが語っていない領域です。ここを仕様として断言させないでください。
表の各行を壊して裏を取る
表ができたら、各行を実装側から壊します。これがミューテーションテストの手作業版です。専用ツールを入れなくても、行ごとに1か所を変えて回すだけで、同じ判断ができます。
best-practicesは、Claudeに「合否が返るもの」を渡すと自分で直しては回し直すループに入ると説明しています。ここではそのループを、直すためでなく壊すために使います。
表の各行について、その挙動を壊す変更を shipping/fee.py に1つだけ入れ、
pytest を実行して、落ちたテスト名を記録して。1行ごとに git restore で戻すこと。
壊す例: 比較演算子の >= を > に変える / 定数を1だけずらす / 例外を握りつぶす。
最後に「壊しても全部通った行」だけを一覧にして。作業は専用ブランチで行い、各行のあとにソースを元に戻させます。戻し忘れが次の行の結果を汚すからです。確認は自分でも一度見ておくと安全です。
git status --short # 変更が残っていないこと
git diff --stat # 壊した変更が戻っていること公式は、完了を伝えられたら「実行したコマンドとその出力」という証拠を求めるよう勧めています。理由は、「テストが通りました」という一文だけでは情報が少なく、出力を読むほうが再実行より速いからです。壊す作業でも同じで、行ごとに落ちたテスト名まで出させます。
結果は3通りに分かれます。
壊した結果の読み方
期待どおりのテストが落ちた
その行は仕様として機能しています。表の記述を仕様書に採用できます。
別のテストだけが落ちた
名前と中身がずれています。表の行を書き直し、落ちたテストの名前で仕様を読み直します。
全部通った
空洞です。その挙動は、テストに守られていないので、仕様としては未確定です。
空洞なテストの見分け方
壊しても通る行には、型があります。
- 境界から遠い入力: 閾値のテストなのに、5000の代わりに100000を使っている。
>=と>の差が出ません。 - 期待値を実装と同じ式で計算している: テスト内で
fee = subtotal * 0.1と書いて比較している。実装を変えたらテストも同じ式を使うため、同時にずれて通り続けます。 - 例外の型を見ていない:
pytest.raises(Exception)だけで、どの例外かを問うていない。 - モックが結果を返している: 検証したい処理そのものがモックに置き換わっている。
最後の型は、Claudeが書いたテストでも起きます。実装に合わせて期待値を書き換える癖への対処は、テストのハードコードを防ぐプロンプトで扱っています。
読む側で押さえたいのは、空洞を見つけたときの扱いです。そのテストを根拠に「仕様はこうです」と書かず、仕様表に「未確定」と明記して残します。common-workflowsの「Work with tests」は、Claudeに境界条件のテストを足させる流れを示しています。空洞な行は、その入口として使えます。ただし書き足す前に、現状の挙動を先に記録する手順が要るので、そこは特性テストの側に渡します。
仕様表を残す
作った表は、会話が終わると消えます。リポジトリに置いておくと、次に同じモジュールを触る人とClaudeが使えます。
# shipping/fee.py の仕様(テストから抽出・壊して確認済み)
| 挙動 | 根拠のテスト | 状態 |
|---|---|---|
| 小計 5000 以上で送料 0 | test_free_shipping_at_threshold | 確認済み |
| 離島は 1200 | test_fee_for_island | 確認済み |
| クーポンと離島の併用 | なし | 未確定 |CLAUDE.md には、この表の場所と、検証コマンドだけを書きます。best-practicesは CLAUDE.md に置くものとして「リポジトリで作業の完了を確かめる方法(テストのコマンド、1ファイルだけ回す方法)」を挙げています。仕様表そのものを CLAUDE.md に貼ると長くなるので、参照先だけにします。
- 送料の仕様: docs/spec/shipping-fee.md(未確定の行は変更しない)
- 1ファイルだけ回す: pytest tests/test_shipping_fee.py表が語れないこと、途中でつまずく点
表から出せる仕様は、テストが書かれた範囲に限られます。ここを取り違えると、仕様書のつもりで誤解を固めます。
- テストが古い挙動を守っていることがある: 仕様変更のあとに直されなかったテストは、いまの運用と食い違ったまま緑です。壊して落ちる行でも、「正しい仕様」とは限りません。業務の人に確かめる行として、表に印を付けておきます。
- バグを守るテストもある: 税の端数処理のように、不具合を含んだ値がそのまま期待値になっている場合です。気づいたら仕様表に「現状の挙動(要確認)」と書き、直す判断は別に切り出します。
- 壊した変更が保存されたまま残る: 行ごとの
git restoreを忘れると、次の行の結果が前の破壊と混ざります。作業前にコミット済みの状態から始め、行ごとにgit status --shortの出力が空になるのを見てから次へ進みます。未コミットの自分の変更があるなら、先に退避してください。 - 遅いテストと不安定なテスト: 1行ごとに全体を回すと時間がかかります。common-workflowsの「Work with tests」が示すとおり、Claudeは既存のテストの書き方に合わせて動くので、対象テストファイルだけを回す指示を足します。落ちたり通ったりを繰り返すテストは、壊した結果の判定に使えません。不安定なテストの調べ方で先に原因を切り分けます。
読み切れないときの分担
モジュールが大きく、テストファイルが数百行に及ぶなら、読む作業をサブエージェントに出す手もあります。best-practicesは、長い出力を主会話に持ち込まない目的で調査をサブエージェントに任せる使い方を挙げています。テスト一覧の抽出だけを任せ、壊して確かめる工程は主会話で見届ける、という分担が扱いやすいでしょう。
大規模なリポジトリで読む範囲を決める考え方は大規模コードベースの探索のコツにまとめています。言語が古く、テストも無い読解の例はPerlの古いスクリプトを読解し移行テストを作るが参考になります。
まとめ
テスト名は仕様の目次で、壊して落ちた行だけが本文です。落ちなかった行を「未確定」として残せば、その後の変更で何を足すべきかも見えます。まず1モジュール、表を5行ほど作って、全行を壊してみるところから始めてください。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。