はじめに
QAの現場でよくあるのが、次のようなやり取りです。
これってFEのバグですか?
それともBEのバグですか?
以前の私は、画面上のエラーだけを見て、
FEのバグです!
とチケットを投げ、Devチームから、
APIは正しいレスポンスを返していますよ
と打ち返されることがよくありました。
今振り返ると、画面上の事象だけで原因を決めつけていたため、開発者側で同じ調査を最初からやり直す必要があり、コミュニケーションコストを増やしてしまっていました。
それ以来、Chrome DevToolsを中心に、必要に応じてAPI仕様、DB、ログまで確認し、どこまでは正常で、どこから期待値とずれているのかを整理してから報告するようにしています。
本記事では、私が実務で使っている不具合の切り分け方と、開発者がすぐ調査を始められるバグ報告の書き方を紹介します。
「FEかBEか」の二択で考えすぎない
現在のWebシステムでは、画面とAPIの間にBFF、GraphQL、API Gateway、キャッシュ、外部APIなどが入ることがあります。
そのため、単純に「FEかBEか」の二択で決めつけると、調査でハマる原因になります。
QAが明確にしたいのは、担当チームよりも次の点です。
どの処理までは期待どおりで、どこから期待値との差が生じているか
原因の最終判断は、ソースコードやサーバーログを確認できる開発者に委ねる場合もあります。
QAは、判断に必要な事実を集め、調査範囲を狭める役割を担います。
私が実務で見ている流れ
基本的には、次の順番で確認しています。
画面上の事象
↓
Console
↓
Network
↓
Request・Response
↓
API仕様との照合
↓
必要に応じてDB・ログ
↓
事実と仮説を分けて報告
最初から原因を決めつけるのではなく、各レイヤーで確認できた事実を積み上げていくイメージです。
1. まず、画面上の事象を具体化する
最初に整理するのは、何が起きたのかです。
最低限、次の情報を押さえます。
- 操作手順
- 使用ユーザーと権限
- テストデータ
- 期待結果
- 実際結果
- 発生環境
- 発生日時
例えば、次の報告だけでは情報が足りません。
ユーザー情報を保存できません。
実務では、次のように書きます。
ユーザー編集画面で氏名を変更し、「保存」を押下した。
期待結果:
変更後の氏名が保存され、ユーザー詳細画面に反映される。
実際結果:
「保存しました」と表示されるが、
画面を再読み込みすると変更前の氏名に戻る。
この時点では、FE・BEのどちらが原因かはまだ判断しません。
2. DevToolsではPreserve logとDisable cacheを使う
Networkタブでは、状況に応じて次の設定を使います。
Preserve logDisable cache
Preserve logを有効にすると、画面遷移後も通信履歴を残せます。
Disable cacheは、古いJavaScriptやキャッシュされたレスポンスの影響を除外したいときに便利です。
ただし、実際のユーザー環境ではキャッシュが有効です。
キャッシュ無効時だけ正常になる場合は、それ自体が不具合の手掛かりになるため、必要に応じてキャッシュあり・なしの両方で確認しています。
3. 最初にConsoleを見る
私が最初に見るのはConsoleです。
ボタンを押した瞬間に、JavaScriptエラーが出ていないかを確認します。
TypeError
ReferenceError
Cannot read properties of undefined
Uncaught Promise rejection
例えば、次のエラーが出ていたとします。
Uncaught TypeError:
Cannot read properties of undefined (reading 'id')
この場合、JavaScriptの処理が途中で止まり、API呼び出しまで到達していない可能性があります。
ただし、Consoleが赤いからといって、すぐにFEのバグと決めつけるのは危険です。
ブラウザ拡張機能のエラーや、今回の操作とは無関係な既知エラーが混ざっていることもあります。
私は次の点を見ています。
- 操作した瞬間に発生したか
- 同じ操作で毎回再現するか
- Stack traceはどこを指しているか
- 今回の事象と関係しているか
Consoleで何も出ていなければ、次はNetworkへ移ります。
4. Networkで対象のRequestを探す
NetworkタブのFetch/XHRを使い、対象操作に対応するRequestを探します。
例えば、「保存」を押したときに、次のAPIが呼ばれる想定だとします。
PUT /api/users/123
Requestが見つからない場合
本来APIが呼ばれるはずなのに、Requestが見つからない場合は、次のような可能性があります。
- クリックイベントが発火していない
- クライアント側バリデーションで止まっている
- JavaScriptエラーが発生している
- ボタンが無効状態になっている
- Requestがキャンセルされている
- Service Workerやキャッシュが応答している
この場合は、もう一度Consoleへ戻ったり、NetworkのAll表示を確認したりします。
「RequestがないのでFEバグ」と即断しないようにしています。
補足:CORSエラーはFEとは限らない
CORSエラーはConsoleに表示されるため、FEの問題に見えがちです。
ただし、原因はサーバーのCORS設定、許可Origin、Reverse Proxy、環境変数、OPTIONSリクエストへの未対応などの場合もあります。
画面にエラーが表示された場所だけで担当を決めず、Request URL、Origin、preflightとして送信されたOPTIONSリクエスト、Access-Control-Allow-OriginなどのResponse Headerまで確認します。
5. Requestが仕様どおりか確認する
Requestが飛んでいれば、次に見るのはURL、Method、Payloadです。
主に確認するのは次の項目です。
- Request URL
- HTTP Method
- Query Parameter
- Path Parameter
- Request Payload
- Content-Type
- 必要なHeader
- Cookieや認証情報
例えば、API仕様上は次のPayloadを送る必要があるとします。
{
"name": "Taro Yamada",
"age": 30
}
しかし、実際には次のRequestが送られていました。
{
"name": "Taro Yamada",
"age": null
}
ageが必須項目であれば、画面入力値の取得やRequestへのマッピングに問題がある可能性があります。
一方で、BE側にも確認ポイントがあります。
- 必須項目のバリデーションがあるか
- 不正な値に対して適切な4xxを返すか
- 入力不備によって想定外の500になっていないか
ひとつの事象に、FE・BE両方の改善ポイントがあることも珍しくありません。
6. Status Codeだけでは判断しない
Requestの次はResponseを見ます。
個人的には、RequestよりResponseを見ている時間のほうが長いです。
特に200 OKが返っていると安心しがちですが、実務ではResponse Bodyに答えがあることも少なくありません。
例えば、次のResponseです。
{
"success": false,
"errorCode": "OUT_OF_STOCK",
"message": "在庫が不足しています"
}
HTTP通信は正常に完了していますが、業務処理としては失敗しています。
画面側がStatus Codeだけを見て「注文完了」と表示していれば、レスポンス処理に問題がある可能性があります。
ただし、業務エラーをHTTP 200で返す設計が正しいかどうかは、プロジェクトのAPI Contract次第です。
QA側の感覚だけで判断せず、Swagger、OpenAPI、設計書などと照合します。
Status Codeごとの主な確認ポイント
| Status | まず確認すること | 注意点 |
|---|---|---|
| 200 / 201 | Response Body、業務結果、DB更新 | 通信成功と業務成功は同じではない |
| 400 | Payload、型、必須項目、API仕様 | FEの送信ミスとBEのValidation不備の両方があり得る |
| 401 / 403 | Token、Cookie、Role、認可設定 | 認証と認可を分けて考える |
| 404 | URL、Path Parameter、データの有無 | API不在とリソース不在は別 |
| 500 | Request、Response、サーバーログ | 発生箇所はサーバーでも、入力値が起点の場合がある |
例えば、500では次のようなケースがあります。
FEが想定外のnullを送信
↓
BEで入力チェックされない
↓
例外が発生
↓
HTTP 500
この場合、改善ポイントは一つとは限りません。
- FE:仕様に合わない値を送信した
- BE:不正入力を4xxとして処理できなかった
「500なのでBEバグです」とだけ書くのではなく、RequestとResponseをセットで残します。
7. APIのResponseと画面表示を比較する
切り分けで特に効果的なのが、ResponseとUIの比較です。
APIは正しいが、UIが違うケース
APIレスポンス:
{
"balance": 1000000
}
画面表示:
残高:100
この場合、画面側の次の処理を疑います。
- 単位変換
- 項目マッピング
- State更新
- 型変換
- 小数点処理
- 表示用共通関数
- キャッシュ
ただし、UIが本当にそのAPIを見ているか、別のAPIやキャッシュデータを参照していないかは確認しておきます。
API自体が違う値を返しているケース
期待値が「残高:1,000」なのに、APIが次の値を返していたとします。
{
"balance": 100
}
この場合は、DBの保存値、計算ロジック、JOIN条件、集計条件、外部APIなどまで調査対象を広げます。
8. 更新系ではDBも確認する
更新系の不具合では、画面やAPIだけでなく、DBに何が保存されたかも確認します。
例えば、ユーザー更新後に次のSQLを実行します。
SELECT
id,
name,
age,
updated_at
FROM users
WHERE id = 123;
主に見るのは次の点です。
- 対象レコードが存在するか
- 期待した値が保存されているか
-
updated_atが更新されているか - 想定外のレコードが更新されていないか
DBが正しくても、APIやUIの変換処理で値が変わることがあります。
そのため、次の流れで見るようにしています。
画面入力
↓
Request
↓
API
↓
DB
↓
Response
↓
UI
なお、システムによっては非同期処理になっていることもあります。
API成功直後にDBへ反映されていない場合は、仕様上の反映タイミングやジョブの状態も確認します。
実務例:注文完了と表示されたが、注文が作成されていない
ここからは、ひとつの不具合を例に切り分けます。
発生した事象
ユーザーが商品を選択し、「注文確定」を押下しました。
画面には次のメッセージが表示されました。
注文が完了しました
しかし、注文履歴にはデータがありませんでした。
Console
操作時にJavaScriptエラーは発生していませんでした。
少なくとも、クライアント処理が例外で途中停止した形跡は確認できませんでした。
Request
次のAPIが呼ばれていました。
POST /api/orders
{
"productId": 1001,
"quantity": 2
}
API仕様と照合した範囲では、Request内容に問題は見つかりませんでした。
Response
HTTP Status Codeは200 OKでした。
{
"success": false,
"errorCode": "OUT_OF_STOCK",
"message": "在庫が不足しています"
}
通信は成功していますが、業務処理としては在庫不足で失敗しています。
DB
対象ユーザーの注文データを確認しました。
SELECT
id,
user_id,
product_id,
quantity,
status,
created_at
FROM orders
WHERE user_id = 123
ORDER BY created_at DESC;
該当する注文レコードは存在しませんでした。
今回確認した範囲では、APIレスポンスとDBの状態は整合しています。
切り分け結果
確認できた事実は次のとおりです。
- APIは在庫不足を返している
- DBに注文レコードは作成されていない
- UIだけが注文成功として表示している
このため、現時点では次の仮説を立てられます。
フロントエンド側で
success: falseを正しく判定せず、成功メッセージを表示している可能性がある。
ソースコードを未確認の段階では、これを確定原因とは書きません。
確認できた事実、現時点の仮説、未確認事項を分けて報告します。
開発者がすぐ調査できるバグ報告
情報が足りない報告
注文できません。
FEのバグだと思います。
これでは、開発者が最初から同じ確認をやり直すことになります。
改善した報告例
## 事象
在庫不足の商品で「注文確定」を押下すると、
注文は作成されていないにもかかわらず、
画面に「注文が完了しました」と表示されます。
## 再現手順
1. 商品ID 1001の商品詳細画面を開く
2. 数量に「2」を入力する
3. 「注文確定」を押下する
## 期待結果
在庫不足を示すエラーメッセージが表示され、
注文完了として処理されないこと。
## 実際結果
「注文が完了しました」と表示されます。
注文履歴に対象注文は表示されません。
## Network確認結果
Request:
```http
POST /api/orders
```
```json
{
"productId": 1001,
"quantity": 2
}
```
Response:
```text
HTTP 200
```
```json
{
"success": false,
"errorCode": "OUT_OF_STOCK",
"message": "在庫が不足しています"
}
```
## DB確認結果
対象ユーザーの注文レコードが
作成されていないことを確認しました。
## 確認できた事実
- APIは`success: false`を返しています
- DBに注文レコードは存在しません
- UIには注文完了メッセージが表示されます
## 現時点の仮説
画面側で`success: false`が
注文成功として処理されている可能性があります。
## 未確認事項
- 画面側のレスポンス処理
- サーバーログ
この形であれば、開発者は調査済みの範囲をすぐに把握できます。
QAとして一番避けたいのは、FEかBEかを外すことよりも、Devが同じ再現確認を最初からやり直すことだと考えています。
DevToolsだけでは分からないこともある
DevToolsは、ブラウザから見える範囲を確認するには非常に便利です。
ただし、サーバー内部の例外、DBトランザクション、非同期ジョブ、マイクロサービス間通信までは追い切れません。
必要に応じて、次の情報と組み合わせます。
- API仕様
- DB
- サーバーログ
- APM
- ソースコード
DevToolsはゴールではなく、最初の切り分けに使う入り口です。
ログを貼る前にマスキングする
調査結果をチケットやSlackへ貼る際は、認証情報や個人情報をそのまま載せないようにします。
特に注意したいのは次の情報です。
- Authorization Header
- Access Token
- Refresh Token
- Session Cookie
- API Key
- メールアドレス
- 顧客データ
例えば、次のようにマスキングします。
Authorization: Bearer ********
user@example.com → u***@example.com
DevToolsのCopy as cURLには、CookieやTokenが含まれることがあります。
私自身も、共有直前にTokenが含まれていることに気づき、慌ててマスキングした経験があります。
ログを共有する前に、認証情報や個人情報が含まれていないかを必ず確認しています。
よくあるハマりどころ
「500だからBEバグ」
500が発生した場所はサーバー側でも、不正なRequestがきっかけになっている場合があります。
Requestと入力バリデーションをセットで確認します。
「200だから正常」
HTTP通信が成功しただけで、業務処理は失敗しているかもしれません。
Response BodyとDB状態まで確認します。
調査時の簡易チェックリスト
事象
- 再現手順、期待結果、実際結果が明確か
- 環境、ユーザー、権限、データを記録したか
Console・Network
- 操作時のエラーと対象Requestを確認したか
- URL、Method、Payload、Response Bodyを確認したか
API・DB・ログ
- API仕様と照合したか
- 必要に応じてDB、非同期処理、ログを確認したか
バグ報告
- 事実、仮説、未確認事項を分けたか
- Tokenや個人情報をマスキングしたか
まとめ
DevToolsを使う目的は、FEかBEかを決めることではありません。
重要なのは、どこまでは正常で、どこから期待値と違っているのかを整理することです。
以前の私は、
FEのバグだと思います。
と報告していました。
現在は、次のように書くことを意識しています。
APIでは業務エラーが返却されていますが、
画面では成功として表示されています。
Request・Responseと再現時刻を添付します。
現時点では、画面側のレスポンス処理に原因がある可能性があります。
これだけでも、開発者とのコミュニケーションはかなりスムーズになりました。
QAの役割は「誰のバグか」を決めることではなく、開発チームが最短距離で原因にたどり着ける材料をそろえることだと考えています。
ここまでお読みいただき、ありがとうございました。