バグ報告を受け取って、ログとスタックトレースを Claude Code に渡し、「この症状を直してほしい」と頼んだ経験はありませんか。私も何度もそうしました。そして、何度も空回りを見ました。Claude が書いてきた修正は一見それらしく、テストも通る。しかし実機で試すと症状が再発します。もう一度ログを貼り、もう一度別の角度から頼む。気づけば 1 時間が溶けています。
この空回りの原因は、Claude Code の能力ではありません。私が「落ちることを確認できる具体的な基準」を先に渡さずに、症状の再現をも委譲してしまっていたことです。バグ修正を Claude Code に委譲する前に、必ず「落ちる再現テスト」を自分で書く、というワークフローを実例とともに整理しました。運用している設計原則と、実際のコード例、委譲時のプロンプトの型まで、手を動かせる形で残します。
なぜ再現テストを先に書くと AI が無駄打ちしなくなるのか
Claude Code に「ログイン後のリダイレクトが失敗する」とだけ伝えると、Claude はまず「症状を再現する方法」から推測を始めます。テストスイートを読み、関係ありそうなファイルを開き、仮説を立てる。ここまでは問題ありません。ところが、仮説に基づいて「こう直せば動くはず」という修正を書き、既存テストが通ることを確認して、作業完了を宣言してしまいます。実機で症状が再発していても、Claude は再発を観測する手段を持っていません。
先に「この条件で呼ぶと絶対に落ちる」というテストを用意して渡すと、景色が変わります。Claude は次の 2 点を明確に区別できるようになります。
- 修正の目的は「このテストを green にすること」である
- 修正が正しいかどうかは「このテストが green になり、かつ他のテストを壊さないこと」で判定する
曖昧な症状の共有ではなく、コードとして定義された契約です。Claude はこの契約を満たすように集中できます。
もう一つ重要な効果があります。再現テストを書く過程で、私自身が症状を言語化し切れていなかったことに気づくことが多いのです。「リダイレクトが失敗する」とは、200 を返しているのに location ヘッダが無いのか、302 を返しているが URL が間違っているのか、それともクライアント側で遷移が止まっているのか。テストを書こうとすると、これを決めざるを得なくなります。結果として、私が Claude に渡す指示も精度が上がります。
落ちるテストを最小化する 4 つの原則
再現テストは短くするほど、Claude が迷わずに修正対象を特定できます。私が運用している原則を 4 つまとめます。
原則 1: 既存のテストスイートの一部として書く
新規ファイルで隔離して書くと、既存のテストヘルパーや fixture を再利用できません。既存スイートに追加する形で書くと、Claude はそのスイート全体を「修正後に落とさない対象」として自然に認識します。tests/bugs/ のようなディレクトリを作り、チケット番号をファイル名に含めておくと、後から「このチケットの再現テスト」を探すのが早くなります。
原則 2: 1 つのテストケースで 1 つの観測可能な事実だけを主張する
「ログインが成功し、かつリダイレクトが正しく、かつセッション Cookie が立つ」という 3 つのアサーションを 1 つのテストに詰めるのは避けます。どれが落ちたのか分かりやすくするため、1 テスト 1 アサーションを原則にします。Claude は「どのアサーションを green にする必要があるか」を読み取って修正に集中できます。
原則 3: プロダクションに近い入出力で書く
ユニットテストの層で再現できる場合はそれでよいのですが、症状がフレームワーク境界(HTTP、DB、キュー、外部 API)で起きているなら、その境界を含める形でテストを書きます。私は FastAPI の例だと TestClient を使って実際にエンドポイントを叩くテストを書きます。モックで潰すと、モックの緩さが見逃しを生みます。
原則 4: 再現条件を「たった一つの変数の操作」に詰める
バグが「特定のユーザーロール × 特定のリクエスト順序」で起きるなら、その 2 つを両方変数化しつつ、テストケースを parametrize で分けます。Claude に「この変数の組み合わせで落ちる」という形を見せると、原因の切り分けがしやすくなります。
具体例: リダイレクト失敗バグの再現テスト
FastAPI で書いている小さな認証エンドポイントで実際に起きた例をもとに、どんな再現テストを書いているかを示します。症状は「セッションが古いユーザーがログインすると、302 ではなく 200 が返る」というものでした。
まず、空回りしていたときに渡していたプロンプトはこうでした。
ログインが 302 を返さないバグがあります。
src/auth/login.py と関連テストを見て直してください。
この状態で Claude Code に任せると、関係ありそうな if 分岐にコードを足し、既存テストが通ることを確認して完了と宣言します。実機では症状が残ります。
再現テストを書いてから渡すようにしたときの一例がこちらです。
# tests/bugs/test_login_stale_session_redirect.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from tests.factories import UserFactory, make_stale_session
client = TestClient(app)
@pytest.mark.parametrize(
"session_age_days, expected_status",
[
(0, 302), # 新しいセッション: 通常通りリダイレクト
(8, 302), # 古いセッション: ここでも 302 を返すべき(現状 200 が返っている)
],
)
def test_login_redirects_regardless_of_session_age(session_age_days, expected_status):
user = UserFactory.create(password="valid-password")
make_stale_session(user, age_days=session_age_days)
response = client.post(
"/auth/login",
data={"email": user.email, "password": "valid-password"},
follow_redirects=False,
)
assert response.status_code == expected_statusポイントは次のとおりです。変数化した session_age_days を 0 日と 8 日で 2 ケース回すことで、「セッションが古いときだけ落ちる」という事実をテストコードの中で主張しています。Claude に渡すプロンプトは、このテストファイルのパスを添えるだけで良くなります。
tests/bugs/test_login_stale_session_redirect.py が現状失敗します。
session_age_days=8 のケースで 200 が返っているのを 302 にしたいです。
既存の全テストを緑に保ったまま、この再現テストを緑にしてください。
Claude Code は、このテストを green にすることに集中できます。そして「既存の全テストを緑に保つ」という制約のおかげで、雑な修正で別の箇所を壊すことも減ります。
委譲・レビューの 3 段階ワークフロー
再現テストが書けたら、Claude Code との協業は 3 段階で回しています。
段階 1: 再現テストを書き、コミットする
修正前に、失敗する状態のテストだけをコミットします。コミットメッセージは test(bugs): reproduce stale session redirect issue (#234) のように「再現テストであり、まだ修正していない」ことを明示します。CI 上で意図通り落ちていることを確認します。後からこのコミットを単体で見たときに、そのチケットが何を問題にしていたかが一目で分かる状態になります。
段階 2: Claude Code に修正を委譲する
プロンプトはシンプルです。再現テストのパス、落としたくない既存テストの範囲、修正対象のディレクトリをまとめて渡します。
【再現テスト】tests/bugs/test_login_stale_session_redirect.py
【落としてはいけない既存テスト】tests/auth/, tests/session/
【修正対象候補】src/auth/, src/session/
上記の再現テストを緑にしてください。
既存テストを 1 つでも壊したら失敗とみなします。
修正方針を 3 行以内で先に説明してから、コードを書いてください。
最後の「方針を 3 行以内で先に説明」は重要です。Claude がどの仮説で直しに行くのかを読んでから実装に入れるので、明らかな誤読があれば私が早い段階で止められます。
段階 3: 差分レビューと「元の変更意図」の検証
Claude の修正が CI で緑になったら、私は 2 つの観点でレビューします。1 つ目は「再現テストが確かに緑になったか」、2 つ目は「修正の本体が、再現テストの動機と一致しているか」です。後者が大事で、Claude は時に「テストを緑にする最小の変更」として本質的でない箇所を触ることがあります。たとえば、古いセッションをそもそもログイン前にクリアすれば再現テストは通りますが、それは症状を隠しているだけです。ここで人間の判断を入れないと、再現テストは緑でも問題の根本は残ります。
レビューで違和感があれば、「今の修正は再現テストを緑にはしているが、〇〇の理由で根本原因を捉えていないように見える。別の方向で修正し直してほしい」と戻します。この戻しも、再現テストがある状態では「向き直し」が容易です。どこに向かって修正するかが変数ではないからです。
再現テストを書けない種類のバグとの付き合い方
正直に書いておくと、再現テストを書きにくいバグも存在します。私がよく遭遇するのは次の 3 種類です。
- 並行処理のタイミングバグ(負荷をかけて 1000 回に 1 回再現するもの)
- ブラウザの特定ビルド × OS の組み合わせでのみ起きる UI バグ
- 再起動や長時間稼働が前提になる状態遷移バグ
これらは通常のユニット層のテストにするのが難しく、書いたとしても不安定 (flaky) になります。この場合は、テストではなく「最小限のスクリプト化された再現手順」を書いて Claude に渡します。たとえば並行処理バグなら、scripts/repro/<issue>.py を用意して「このスクリプトを N 回実行すると数回は AssertionError で落ちる」という状態を作ります。Claude には「このスクリプトで再発しなくなる修正をしてほしい。CI 用の安定テストは別途検討する」と伝えます。再現を「完全に再現性のあるテスト」から「検証のしやすい実験」へ切り下げるだけでも、委譲の精度は大きく変わります。
運用してみて変わったこと
このワークフローを続けて、私側に 3 つ変化がありました。
1 つ目は、バグチケットを読んだ瞬間の反応が「まずテストを書いてみよう」に変わったことです。以前は「Claude に見せる前に少し自分で調べるか」と曖昧にコードを読み始めて、時間が溶けていました。今は、最初の 10 〜 20 分を「落ちるテストを書く」に投資します。書けなければ、そもそも自分が症状を理解していないサインです。
2 つ目は、Claude Code の修正品質が上がったというより、Claude の修正を受け入れるかどうかの判断が速くなったことです。再現テストの緑 / 既存テストの緑という 2 つの境界線があるので、「これは受け入れてよい修正か」を迷わずに判断できます。
3 つ目は、テストスイートそのものが育つことです。バグごとに 1 つずつ再現テストが蓄積されていき、半年もすれば「過去に踏んだバグはすべて回帰テストとして残っている」状態になります。これは個人開発で長期運用するときに、実に大きな財産になります。
全体を振り返って的に: 明日やることは何か
この記事を読んで、もし次のバグチケットから試すなら、具体的な最初の一歩は「再現テストのファイルを 1 つ、失敗する状態でコミットする」ことです。それだけで、Claude Code への委譲の精度がどう変わるかが体感できます。テストが書けない場合は、「書けなかった」こと自体を記録しておいてください。どんな種類のバグだとテストが書きにくいのかが見えてきて、自分の技術的な伸びしろが具体的になります。Claude Code は賢いアシスタントですが、「何に向かって修正すればよいか」の線を引くのは、まだ当面は人間の仕事だと私は感じています。