はじめに
バグ調査では、原因を早く思いつくことより、発生している現象を分解し、「どこまでは正常で、どこからがおかしいのか」を順番に狭めることが重要です。
この記事では、バグ修正で重要になる原因切り分けの考え方を整理します。特定の言語やフレームワークではなく、Webアプリケーション、API、バッチ、モックサーバーなどに共通するデバッグの進め方を扱います。
バグ修正は「怪しい場所を当てるゲーム」ではない
不具合が起きたとき、思いつく原因を並べることは簡単です。
例えば、画面からAPIを呼び出して期待したレスポンスが返らない場合、次のような原因が考えられます。
- フロントエンドの実装がおかしい
- リクエストパラメータがおかしい
- APIがおかしい
- モックサーバーがおかしい
- テストデータがおかしい
- ネットワークがおかしい
- 設定ファイルがおかしい
しかし、この時点では候補を挙げただけです。
「APIが怪しい」「設定がおかしい気がする」といった推測を増やしても、原因には近づきません。
バグ修正で必要なのは、候補を増やすことではなく、確認によって候補を減らすことです。
原因切り分けでは正常な範囲を確定する
デバッグでは、いきなり真因を当てる必要はありません。
重要なのは、処理経路を分割して、一つずつ正常かどうか確認することです。
例えば、次の構成を考えます。
画面
↓
BFF
↓
バックエンドAPI
↓
外部APIまたはモックサーバー
画面に期待した結果が表示されない場合、確認する境界は次のように分けられます。
- 画面からBFFへ、期待したリクエストが送られているか
- BFFからバックエンドAPIへ、期待した値が渡っているか
- バックエンドAPIが外部APIやモックへ到達し、期待した値を受け取っているか
- 境界ごとの変換で値が欠落していないか
- 最後に画面が、そのレスポンスを正しく描画しているか
例えば4まで正常だと確認できれば、1〜4を何度も疑う必要はありません。
このように、「原因を探す」というより「正常な範囲を確定して境界を狭める」と考えた方がデバッグしやすくなります。
「事実」と「推測」を分ける
原因切り分けがうまく進まないとき、事実と推測が混ざっていることがあります。
例えば次の二つは似ていますが意味が異なります。
モックの設定がおかしい
これは推測です。
一方、次は観測した事実です。
リクエストはモックサーバーまで到達しているが、
意図したStubにはマッチしていない
後者まで確認できれば、調査対象はかなり狭まります。
デバッグ中は、メモやチャットでも次のように分けると整理しやすくなります。
事実:
- リクエストはサーバーまで到達している
- HTTPステータスは200
- 期待したJSONとは異なるレスポンスが返っている
未確認:
- 意図したモック定義にマッチしているか
- より汎用的な定義にマッチしていないか
仮説:
- URLのマッチ条件が広すぎる可能性がある
仮説は必要ですが、事実と同じ扱いにしないことが重要です。
「呼ばれた」と「意図した処理が呼ばれた」は違う
バグ調査では、あるコンポーネントまで処理が到達したことを確認して安心してしまうことがあります。
しかし、到達したことと、期待した処理が実行されたことは別です。
モックサーバーを例にすると、次の状態があり得ます。
リクエストがモックサーバーに到達した
↓
何らかのStubにマッチした
↓
しかし意図したStubではなかった
URLの正規表現やワイルドカード条件が広い場合、より汎用的なStubに吸われることがあります。
そのため、「リクエスト履歴がある」だけでは確認として不十分です。
見るべきなのは、例えば次の情報です。
- どのリクエストが届いたか
- マッチしたか、未マッチだったか
- どの定義にマッチしたか
- 実際に何が返却されたか
- 期待した定義との条件差は何か
この一段深い確認が、原因切り分けでは重要です。
調査が停滞するときに起きやすいこと
怪しいものを横並びにする
例えば、次のように候補だけを並べるとします。
クライアントかもしれない
APIかもしれない
モックかもしれない
設定かもしれない
候補は増えていますが、何も否定できていません。
これでは調査範囲が広いままです。
一方、切り分ける場合は、確認済みの事実を増やします。
クライアントからリクエストは送られている
↓
モックサーバーにも到達している
↓
未マッチではない
↓
どのStubにマッチしたか確認する
一つ確認するたびに、次に見る場所が限定されます。
ログを「読む」だけで終わる
ログを見ること自体がデバッグではありません。
重要なのは、何を確認するためにそのログを見るのかです。
例えばアクセスログを見る目的は、単にエラーを探すことではなく、
このリクエストはこのサーバーまで到達したか
を確認することかもしれません。
同様にSQLログなら、
期待した条件でクエリが実行されたか
を確認します。
目的のないログ閲覧は、情報量が増えるだけで原因の特定につながりにくくなります。
一度に複数箇所を変更する
調査中に複数の設定やコードを同時に変更すると、直ったとしても原因が分からなくなります。
例えば次を同時に変更したとします。
- URLのマッチ条件を変更
- テストデータを変更
- 起動オプションを変更
- キャッシュを削除
これで正常になっても、どの変更が効いたのか判断できません。
原因を特定したいなら、可能な限り一度に一つの条件を変えます。
効率のよい確認は「可能性を大きく減らす」
価値が高いのは、一回の確認で多くの可能性を消せる調査です。例えば画面表示がおかしい場合、最初にNetworkタブでレスポンスを確認します。レスポンスが誤っていればサーバー側を、正しければ画面側を優先して調べられます。
良いデバッグ質問は確認する境界を指定する
「APIがおかしくないか」のような質問だけでは、確認する対象が広すぎます。
調査を進める質問は、境界と期待値を指定します。
悪い確認:
APIがおかしくないか?
良い確認:
画面からBFFへ送ったリクエストと、BFFが返したレスポンスは期待どおりか?
例えば、次のように聞き換えられます。
- SQLが実行されたかではなく、期待した条件と値で実行されたか
- モックに到達したかではなく、期待したStubにマッチしたか
- 画面が壊れているかではなく、Networkタブで確認できるレスポンスは正しいか
確認結果を「はい」か「いいえ」で答えられる形にすると、次に見る場所が決まります。調査状況を他の人へ共有するときも、事実と未確認の境界が伝わりやすくなります。
再現条件を固定すると切り分けやすくなる
再現条件が毎回違う状態では、原因を切り分けることが難しくなります。
まず次を固定します。
- 操作手順
- 入力値
- 使用するデータ
- 実行環境
- 発生するエラー
- 期待結果
例えば、
検索 → 詳細画面 → 特定ボタン押下
でだけ発生するなら、その手順を固定します。
「たまにおかしい」という状態から、「この条件なら再現する」に変わるだけで、比較実験ができるようになります。
正常系との差分を見る
不具合だけを眺め続けるより、正常に動くケースと比較した方が原因を見つけやすいことがあります。
比較する対象には次があります。
- 正常なリクエストと異常なリクエスト
- 正常なSQLと異常なSQL
- 正常環境と異常環境の設定
- 正常データと異常データ
- 正常時と異常時のログ
例えば正常なリクエストでは特定のHTTPヘッダーがあり、異常時だけ存在しないなら、調査対象をその差分周辺へ絞れます。
重要なのは「異常なものを理解する」だけでなく、「正常時と何が違うのか」を見ることです。
私がバグ修正で意識している順番
私はバグ調査をするとき、概ね次の順番で考えます。
現象を確認する
↓
期待結果との差を明確にする
↓
再現条件を固定する
↓
処理経路を分割する
↓
どこまで正常か確認する
↓
事実から仮説を立てる
↓
一つずつ検証する
↓
最小の修正を行う
↓
再発防止のテストを追加する
特に重要なのは、修正コードを書く前に、どこまで正常かを確認する部分です。
原因が分からないままコードを変更すると、偶然直ることはあっても、別の不具合を作る可能性があります。
原因を説明できるところまで確認する
バグが直っただけでは、調査が完了したとは限りません。
理想的には、次の説明ができる状態まで確認します。
この条件で不具合が発生していた。
原因はこの処理だった。
この値がこうなるため、最終的にこの現象になっていた。
この修正によって原因を取り除いた。
同じ条件の回帰テストを追加した。
「いろいろ触っていたら直った」では、この説明ができません。
説明できない修正は、同じ問題が再発したときにもう一度最初から調査することになります。
まとめ
重要なのは、複雑な現象を小さな確認項目へ分解し、一つずつ可能性を消していくことです。
特に次を意識すると、原因を切り分けやすくなります。
- 事実と推測を分ける
- 再現条件を固定する
- 正常系との差分を見る
- 処理経路を境界ごとに確認する
- 「到達した」と「意図した処理が動いた」を区別する
- Yes / Noで次の調査先が決まる質問をする
- 一度に一つの条件を変更する
- 修正後に原因を説明できる状態にする
- 回帰テストで再発を防ぐ
デバッグは、勘で真因を当てる能力ではありません。
「どこまでは正常か」を積み上げ、問題が存在する境界を狭めていく作業です。