PRレビューツールが見逃すバグを、ClaudeにNext.js+Zustandのコードを渡して3つ発見した話
はじめに
npm run testも通った。ESLintも問題なし。TypeScriptの型チェックも通過。でも、実際に画面を操作したら壊れる——そういうバグが存在します。
AIコードレビューの世界では「AIにテストコードを書かせると、バグを含んだ実装コードをそのまま正解としてテストを生成してしまう」という問題がよく指摘されています。コードを見せてテストを生成させると、バグを含んだコードを「正」としてテストが書かれてしまうのです(同語反復の罠)。
でも今回やったのは、テストを書かせることではありません。機能単位でコードをClaudeに渡し、潜在バグを探させることでした。
特別なプロンプトエンジニアリングは何もしていません。「フォーム送信機能の潜在的なバグを発見し、ghコマンドでIssue化してください」とシンプルに投げただけです。それでも、Claudeはコンポーネント・状態管理・APIの関係を読んで、操作しないと出ないバグを自分で推論して見つけてきました。
結果、Jest・ESLint・TypeScriptの型チェックをすべてクリアしていた1つのフォーム送信機能から、3つの潜在バグが明らかになりました。冷や汗が出た話を共有します。
AIにバグを探させることは、世界的なトレンドになっている
まず大きな文脈として整理しておきます。
AIコードレビュー市場はすでに急拡大しており、AIコードレビュー市場は2024年の67億ドルから2030年には257億ドルへの成長が予測されており、開発者の84%がAIツールを使用し、新しいコードの41%がAI支援で生成されるまでになっています[1]。DORA 2025レポートによれば、AIコードレビューを活用した高パフォーマンスチームはバグ検出精度が42〜48%向上しており、従来の静的解析ツールの20%未満と比べて大幅に改善されています[2]。
「AIにバグを探させる」という発想自体は、すでに世界的な標準に近づいています。CodeRabbit・Cursor Bugbot・GitHub Copilot Code Reviewといったツールが、PRに自動でバグ指摘コメントをつける形で広く使われるようになっています。
ただし、これらのツールには共通の弱点があります。
「本当のボトルネックはバグ検出ではなくコンテキストだ。AIツールはすべてのPRをコードの断片として扱い、ビジネスロジックや機能の文脈を無視している」[3]という指摘があります。つまり、PRの差分だけを見るツールは「この機能がどう使われるか」という操作の文脈を持てないため、操作フローをまたぐバグには弱いのです。
今回のアプローチは、機能単位でコンテキストをまとめてClaudeに渡すことでこの問題を意識的に回避しています。世界的なトレンドの「次の一手」として位置づけられると思っています。
そもそもJestやLinterで何が検知できて、何ができないのか
まず前提を整理します。各ツールには明確な得意・不得意があります。
ツール別の得意領域
| ツール | 何を見るか | 得意なこと | 苦手なこと |
|---|---|---|---|
| Linter(ESLint等) | コードの構文・スタイル | インデント、未使用変数、構文エラー | ロジックの意図、画面をまたぐ挙動 |
| 型チェック(TypeScript/tsc) | コードの型・構造 | 型の不一致、存在しないプロパティへのアクセス | 実行時の状態変化 |
| Jest / Vitest(単体テスト) | 関数・コンポーネント単体 | 正常値・異常値の入出力検証 | UIや操作フローが絡む挙動、コンポーネント間の連携 |
| AIレビュー(今回のアプローチ) | コード+操作の文脈 | ロジックの穴、エッジケースの推論、仕様の矛盾 | 文法・スタイルの確定的チェック |
また「テストカバレッジはコードの実行行数を測るが、テストの品質(バグを本当に捕まえられるか)は測らない」という指摘もあります[4]。カバレッジ100%でもバグが残る理由はここにあります。
一方でAIコードレビューについては、「Linterは構文・スタイルの高速・確定的なチェックに優れているが、コードの意図を推論したり、アーキテクチャのコンテキストを理解したり、ルールベースのチェックをすり抜けた微妙なバグを捕まえたりすることはできない」という整理がされています[5]。
つまり、JestやESLintは「コードが正しく書かれているか」を見るツールであり、「操作フローが仕様通りに動くか」を見るツールではないのです。
「AIにテストを書かせる」と「AIにバグを探させる」の違い
AI駆動開発が広まるにつれて、「AIにテストを書かせる」というアプローチも一般的になっています。しかしこの2つは目的も効果もまったく異なります。
| AIにテストを書かせる | AIにバグを探させる(今回) | |
|---|---|---|
| AIへのインプット | 実装コード | 実装コード+関連ファイル一式 |
| AIのアウトプット | テストコード | バグの指摘+Issue |
| 問題点・限界 | バグを含んだコードを「正」としてテストを書いてしまう(同語反復の罠) | 推論ベースのため見落としはある |
| 何が見つかるか | 実装通りに動くかどうか | 実装の穴・操作フローの矛盾 |
| E2Eテストとの関係 | 単体テストの補完 | E2Eテストを書く前のリスク洗い出し |
重要なのは、AIにテストを書かせる前に、バグを探させる方が理論上は正しい順序だということです。バグが残ったままテストを生成させると、そのバグを「正しい挙動」としてテストが書かれてしまうからです。
ただ今回は、先にAIでテストを生成した後にバグ発見をAIにやってもらいました。それでも3つの潜在バグが検出できたという事実は、逆に言えば「すでにテストがある状態でもバグ探しは有効」であることを示しています。理想の順序はバグ発見→修正→テスト生成ですが、既存のプロジェクトに後から導入する場合でも十分機能します。
環境
- フロントエンド:Next.js + Zustand(状態管理)
- バックエンド:FastAPI
- 使用ツール:VSCode + GitHub Copilot拡張(モデルをClaude Sonnetに指定)
- Issue化:GitHub CLI(
ghコマンド)
GitHub Copilot拡張はデフォルトではGPT系モデルですが、チャット画面でモデルをClaude Sonnetに切り替えることができます。今回はこの方法で使いました。
やったこと
VSCode上のCopilotチャットを開き、対象機能を絞って以下のプロンプトを投げました。
フォーム送信機能の潜在的なバグを発見し、ghコマンドでIssue化してください
これだけです。特別なロールプレイ指示や操作手順の入力は何もしていません。追加でコンテキストとして、関連するコンポーネントファイルと状態管理(Zustand store)のファイルをチャットに添付しました。
「機能単位で絞る」ことが重要だった
ここに一つ気づきがあります。プロジェクト全体のコードを渡すのではなく、「フォーム送信機能」という機能単位に絞ってコードを渡したことが、バグ発見の精度に直結していたと感じています。
機能を絞ることで、Claudeはその機能の中でどんな操作が起きうるかを集中して推論できます。Reactコンポーネント・Zustand store・FastAPI側のエンドポイントが1つの機能の中でどう連鎖するかを読んで、「この操作をしたときにこの状態はどうなるか」というシナリオを自分で補完してくれます。
逆に言うと、機能の範囲が広すぎると推論が分散して精度が下がる可能性があります。「画面単位」「フォーム単位」くらいの粒度が実用的だと感じました。
「バグを探して」だけではなく、「Issue化まで」をセットで依頼したのもポイントです。
あえて数を指定しなかった
「3つ見つけてください」のように数を指定しませんでした。これは意図的な判断です。
数を指定すると、実際のバグが1つしかなかった場合でも、Claudeが「3つ出さなければ」というプレッシャーで存在しないバグを作り出すリスクがあります。バグの数はコードの状態によって変わるものなので、数を固定するとAIの出力がその数に引っ張られてしまいます。
「潜在的なバグを発見してください」と数を開放しておくことで、Claudeが本当に問題だと判断したものだけを報告するようになります。今回たまたま3つ出てきましたが、機能によっては1つだったり、0だったりすることもあります。それで正しいはずです。
発見されたバグ3つ
バグ① バリデーションが通過してしまう
フォームにはクライアント側のバリデーションを実装していました。しかし特定の操作順序を踏むと、バリデーションが走らずに送信できてしまうケースがありました。
「正常値」「異常値」の単体テストは書いていましたが、「画面遷移後に戻ってきた状態で送信する」という操作フローは想定できていませんでした。Claudeはコードと状態管理の流れを見て、この穴を指摘しました。
Jestで検知できなかった理由:単体テストは「入力値の正誤」しか見ない。Zustandの状態が前の画面から引き継がれた状態で送信ボタンが押されるというシナリオは、コンポーネント単体のテストでは再現できない。
バグ② フィルタリング条件が甘い
フォームに絞り込み条件を持つUIがあり、バックエンド(FastAPI)にその条件を渡してフィルタリングしていました。しかし複数条件を組み合わせたときの挙動が考慮されていませんでした。
具体的には、フロントの状態管理上で「条件AとBを同時に指定したとき」のパラメータの組み合わせが想定されておらず、APIに意図しないクエリが渡るケースです。
Jestで検知できなかった理由:各条件を個別に渡すテストはあったが、複数条件の組み合わせパターンのテストがなかった。「単体テストは個々のコンポーネントにフォーカスし、システム全体の挙動や異なるモジュール間の連携の問題を検知できない」[6]という限界がここに出た。
バグ③ モーダル操作でフォームがリセットされる
これが一番インパクトがありました。
モーダルを開いてフォームを入力中に、特定の操作(モーダルを一度閉じて再度開くなど)をすると、入力内容がリセットされてしまうバグです。
ZustandのstoreがReactコンポーネントのライフサイクルと噛み合っておらず、モーダルのアンマウント時に状態がクリアされていました。「実際に操作してみないと絶対に気づけない」種類のバグですが、Claudeはコンポーネントとstoreの関係を見て「モーダルのアンマウント時に状態がリセットされる可能性があります」と指摘しました。
Jestで検知できなかった理由:Zustandの状態変化はReactコンポーネントのマウント・アンマウントのライフサイクルと密結合しており、モーダルの開閉という操作シナリオを再現しないと現れない。Testing Libraryを使えば検知できたが、そのテストケース自体を思いつけていなかった。
Issue化の流れ
Claudeはバグを指摘した後、そのままGitHub CLIのコマンドを提示してくれました。
gh issue create \
--title "フォーム送信:特定操作順序でバリデーションがスキップされる" \
--body "## 概要\n特定の操作順序を踏むとクライアントバリデーションが実行されずに送信できてしまう。\n\n## 再現手順\n1. フォームページに遷移する\n2. ...\n\n## 期待される動作\nバリデーションが必ず実行されること\n\n## 実際の動作\n送信が通ってしまう"
「このコマンドを実行しますか?」と確認が入り、許可するとそのままGitHubにIssueが作成されました。事前にgh auth loginでログイン済みであれば、追加の設定は不要です。
3つのバグそれぞれに対してIssueが自動作成され、タイトル・再現手順・期待動作まで整形された状態でGitHubに入りました。
なぜClaudeが見つけられたのか
ここが今回の本質だと思っています。
通常のAIテスト生成は「コードを見てテストを書く」ため、コードに含まれたバグをそのまま正解として扱ってしまうという問題があります。
今回のアプローチは違います。Claudeは「コードを読んで操作シナリオを自分で補完する」ことをしています。Reactコンポーネント・Zustand store・FastAPIエンドポイントを機能単位でまとめて渡すと、それぞれの関係性から「このコードが実際に動くとき、何が起きるか」を推論します。
具体的には次のような推論が働いていたと考えられます。
- Zustand storeの初期化タイミングを見て「このコンポーネントがアンマウントされたとき、storeはどうなるか」を読む
- フィルタリングの条件パラメータを見て「複数条件が同時に渡ったとき、フロントの状態はどう変化するか」を読む
- バリデーションのトリガー箇所を見て「別の経路で送信ボタンが押されたとき、バリデーションは必ず走るか」を読む
AIコードレビューの特性として「コードの意図を理解し、アーキテクチャのコンテキストを把握し、ルールベースのチェックをすり抜けた微妙なバグを捕まえる」[5:1]という点があります。今回はまさにこれが機能しました。
「機能単位でコードをまとめて渡す」ことで、Claudeが操作シナリオを自力で補完しやすくなる——これが今回の一番の発見でした。
やってみて思ったこと
- プロンプトはシンプルで十分。「〜の潜在的なバグを発見し、ghコマンドでIssue化してください」で動いた
- 機能単位で絞って渡すのが重要。プロジェクト全体より「この画面のこの機能」という粒度の方が精度が上がる
- 関連ファイル(コンポーネント + store + APIエンドポイント)をセットで添付すると、関係性の推論精度が上がる
- Issue化まで一気にやってくれるのが思った以上に便利。発見→記録のサイクルが速い
これは「総合テスト」の代替ではない
一点、誤解のないように補足します。
今回のアプローチは、システムを実際に動かすE2E(エンドツーエンド)テストや統合テストの代替にはなりません。Claudeはコードを静的に読んで推論しているだけで、実際にブラウザを操作したり、DBに接続して動作を確認したりはしていません。
位置づけとしては以下のイメージです。
| テストの種類 | 何をするか | コスト | 今回との関係 |
|---|---|---|---|
| 単体テスト(Jest/Vitest等) | 関数・コンポーネント単体を動かして検証 | 低 | すり抜けたバグを今回が補完 |
| 今回のアプローチ | コードを静的に読んで操作シナリオを推論 | 低〜中 | 単体テストと統合テストの間を埋める |
| 統合テスト | 複数モジュールを組み合わせて動かして検証 | 中 | より確実な検証が必要な箇所に |
| E2Eテスト(Playwright等) | 実際にブラウザを動かしてユーザー操作を再現 | 高 | 最終的な品質担保 |
今回見つかったバグは「単体テストをすり抜けていたが、統合テストやE2Eテストを書けば検知できた種類」のものです。ただ、統合テストやE2Eテストは環境構築や実行コストが高い。Claudeによる静的な推論レビューは、そのコストをかける前に「どこを重点的にテストすべきか」を絞り込むための手段として使えると感じています。
バックエンド(Python / FastAPI)にも応用できる
今回はNext.js+Zustandのフロントエンドが中心でしたが、同じ手法はバックエンドにもそのまま使えます。
FastAPIのエンドポイントとPydanticのスキーマ、依存関係(Depends)を機能単位でまとめてClaudeに渡せば、同様に「Pytestで書いた単体テストをすり抜けるバグ」を推論してくれます。
# バックエンド向けのプロンプト例
フィルタリングAPIの潜在的なバグを発見し、ghコマンドでIssue化してください
渡すファイルのイメージ:
# 渡すファイル例
- routers/items.py # エンドポイント定義
- schemas/item.py # Pydanticスキーマ
- services/item.py # ビジネスロジック
- dependencies.py # 共通のDepends
フロントと同様に、「エンドポイント単位」か「機能単位」で絞って渡すのがポイントです。Pytestで単体テストは書いているが、複数のエンドポイントをまたぐシナリオや、クエリパラメータの組み合わせ漏れなどを見つけるのに向いています。
まとめ
| アプローチ | 何を見るか | 何が見つかるか |
|---|---|---|
| ESLint / tsc | 構文・型 | インデントや型の不一致などの表層的な問題 |
| Jest / Vitest(単体テスト) | 関数・コンポーネント単体 | 正常値・異常値の入出力ミス |
| 今回のアプローチ | コード+操作シナリオの推論 | 操作しないと出ないバグ、状態管理の穴、条件の組み合わせ漏れ |
| E2Eテスト(Playwright等) | 実際のブラウザ操作 | ユーザーが実際に踏む操作上のバグ |
「テストをすり抜けるバグを見つけたい」「E2Eテストを書く前にリスクを洗い出したい」という方は、一度試してみてください。思ったより簡単に動きます。
参考
使用モデル:Claude Sonnet(GitHub Copilot拡張経由)
Discussion