はじめに
前回、Chrome DevToolsを使って「これってFE?BE?」を切り分けるときに見ているポイントをまとめました。
今回はその続きで、APIまでは呼ばれているのに、期待した結果にならない場合にどう調査するかを書きます。
QAをしていると、こういう不具合によく出会います。
- 保存ボタンを押した。APIも200で返ってきている。でも再読み込みすると元の値に戻っている。
- APIのResponseは正しそう。でもDBの状態と一致していない。
200 OKが返っている、画面にエラーが出ていない――それだけでは、どこで問題が起きているのか正直わかりません。
私が更新系の不具合を見るときに、だいたい追っている流れはこんな感じです。
画面入力 → Request → Response → DB → 再取得 → 画面表示
※ これはシステム内部の処理順序ではなく、私が調査で確認していく順序です。
この記事では、APIテストのやり方そのものではなく、実際に不具合が起きたときにRequest・Response・DBをどうつなげて見るかに絞って書きます。
最初から「APIのバグ」と決めない
例えば、ユーザー編集画面で氏名を「山田 太郎」から「山田 次郎」に変更して保存したとします。
「保存しました」と表示されたのに、画面を再読み込みすると「山田 太郎」に戻っている。
この時点で「保存できていないからBEのバグ」と判断するのは、正直まだ早いです。
考えられる可能性はいくつもあります。
- FEが変更後の値を送っていない
- APIは受け取ったが更新していない
- 別レコードを更新している
- DBは更新されたが、取得APIが古い値を返している
- キャッシュされた値を表示している
- 更新後に別処理で値が上書きされている
なので、まずRequestから見ていきます。
1. Request:そもそも何を送ったのか
最初に見るのは、実際に送信されたRequestです。
仕様上は次のRequestを想定しているとします。
PUT /api/users/123
{
"name": "山田 次郎",
"departmentId": 10
}
実際のRequestも同じであれば、少なくともブラウザからAPIまでは変更後の値が送られている、という事実が一つ増えます。
逆に、送られているのが"name": "山田 太郎"のままだったら、API以前の処理を疑うことになります。
Payloadだけじゃなく、対象IDも見る
更新系で意外とやりがちなのが、値だけ見てIDを見落とすパターンです。
PUT /api/users/124に対して{"name": "山田 次郎"}を送っていたら、Payload自体は正しくても更新対象がそもそも違います。
実際、私も一度これで無駄に1時間くらいAPI側を疑ったことがあります。
なので、Requestを見るときは、
- URL / Method
- Path Parameter / Query Parameter
- Payload
- 必要なHeader
をセットで確認するようにしています。
「値が合っているか」だけでなく、誰の、どのデータを、何に変更しようとしているRequestなのかまで見る感じです。
2. Response:HTTP Statusだけで終わらせない
Requestが仕様どおりなら、次はResponseです。
HTTP 200 OKが返ってきたからといって、それだけで「API正常」とは判断しません。Response Bodyまで見ます。
{
"success": true,
"user": {
"id": 123,
"name": "山田 次郎"
}
}
ここまで確認できれば、
- Requestには変更後の値が入っている
- APIは成功としてResponseを返している
- Responseにも変更後の値が入っている
というところまでは事実として言えます。
ただし、これでもDBへの更新が正しく完了したとは限りません。
更新系では、ここを見落としやすいと感じています。
「200 OK」と「更新成功」は別物
例えば、Responseが次の内容だったとします。
{
"success": false,
"errorCode": "VALIDATION_ERROR"
}
HTTP Statusは200でも、業務処理としては失敗しています。
逆に204 No Contentを返す設計なら、Response Bodyが空でも正常です。
私はStatus Codeだけでは判断せず、そのAPIで何を「成功」としているのかを仕様と照らして見るようにしています。
SwaggerやOpenAPI、設計書があれば、実際のRequest・Responseと照合します。
3. DB:何が保存されたのか
更新系でDBを確認できる環境なら、実データを見ます。
SELECT
id,
name,
department_id,
updated_at
FROM users
WHERE id = 123;
結果が変更後の値になっていれば、少なくとも確認した時点ではDBに反映されています。
逆に変更前の値のままなら、
Request:変更後の値
Response:success
DB:変更されていない
という状態になります。
ここまで分かれば、調査範囲はかなり狭くなります。
それでも「BEの更新処理が原因です」とはまだ書きません。
QAから言えるのは、
APIは成功としてResponseを返しているが、確認したDBレコードには変更が反映されていない
というところまでです。
実際の原因がTransactionなのか、非同期処理なのか、別DBへの書き込みなのかは、サーバーログやコードを見ないと分かりません。
DBが正しいからAPIも正しい、とは限らない
逆のパターンもあります。
DBには正しい値があるのに、取得APIが古い値を返しているケースです。
考えられるのは、例えば次のようなところです。
- Query条件
- JOIN
- View
- Read Replica
- Cache
- 別テーブル参照
- データ変換処理
ここでも「DBが正しいからFEのバグ」と飛ばさないようにしています。
DB → 取得処理 → Response → UI
の間にも処理があるからです。
4. 更新APIだけでなく「再取得」まで見る
個人的に、これがけっこう重要だと思っている部分です。
保存APIだけ見て終わらず、保存後に画面がどのAPIからデータを取得しているかまで確認します。
例えば、PUT /api/users/123が成功したあと、GET /api/users/123が呼ばれているとします。
PUTのResponseは正しいのに、GETのResponseが古い値を返している場合があります。
このとき、
保存APIは正しいResponseを返しているのでFE側の問題
と判断すると、切り分けを間違えることがあります。
実際には、GET側で古いデータが返っているだけかもしれません。
なので更新系の不具合では、
保存Request
↓
保存Response
↓
DB
↓
再取得Request・Response
までを一つの流れとして見るようにしています。
5. 「DBと違う」ときは、まず本当に同じデータか確認する
これは実務で何度かハマったところです。
APIとDBの値が違ったので不具合だと思って調べたら、そもそも見ているレコードが違った、ということがあります。
DBを見る前に、最低限これくらいは確認しています。
- 接続している環境
- Database / Schema
- Company / Tenant
- User ID
- 対象データのID
- 論理削除の有無
特にマルチテナントのシステムでは、id = 123だけでは足りないことがあります。
SELECT *
FROM users
WHERE company_id = 10
AND id = 123;
のように、実際のデータ構造に合わせて確認します。
また、ID自体は合っていても、Requestで使っている認証情報が想定と違うユーザーのものになっていて、対象がズレているケースもあります。
DBで値を見つけたこと自体より、APIが扱っているデータと本当に同じレコードを見ているかのほうが重要です。
6. DBを「常に正解」とは考えない
DBを確認できるようになると、
DBにこう入っているから、これが正しい値
と考えたくなります。
ただ、DBはあくまで現在保存されている状態であって、仕様上の期待値とは限りません。
例えば、金額を計算するシステムで、
画面:10,000円
API :10,000
DB :9,000
だったとします。
このとき、「DBが9,000だからAPIが間違っている」とは言えません。
仕様上10,000円が正しいのであれば、DBへの保存値のほうが誤っている可能性もあります。
判断基準はDBではなく、あくまで仕様・業務ルールです。
私はDBを「正解を確認する場所」というより、処理の途中で実際に何が保存されたかを確認する場所として使っています。
7. すぐにDBへ反映されないケースもある
APIが成功しても、すぐにDBの最終状態が変わるとは限りません。
システムによっては、
API
↓
Queue
↓
Worker / Job
↓
DB更新
のような非同期処理になっていることもあります。
例えば、
{
"status": "accepted"
}
というResponseが返った直後にDBを確認して、
更新されていないので不具合
と判断すると、仕様上正しい処理をバグとして報告してしまう可能性があります。
同期処理か非同期処理か、反映までの想定時間、Jobの状態、Retryの有無などを確認します。
もう一つ見落としやすいのが、同じレコードに対して複数のRequestがほぼ同時に飛んでいるケースです。
保存ボタンの連打や、別のバッチ処理による更新で、後から来たRequestに値が上書きされることもあります。
これはRequest・Response・DBを個別に見るだけでは気づきにくく、時系列で並べて初めて分かることがあります。
DBまで見るなら、同期処理なのか非同期なのか、同じデータを更新する別処理があるのか、といったところも意識するようにしています。
8. 再現した時刻を残す
ログ調査が必要になりそうな不具合では、再現時刻を必ず残しています。
可能であれば、
- User ID
- Request URL
- 対象データID
- Request ID / Trace ID
あたりも一緒に残します。
開発者に「さっきエラーになりました」と伝えるより、
2026/09/03 10:15:32 JST
user_id: 123
PUT /api/users/123
まで分かっていたほうが、ログを探す範囲をかなり狭められます。
QA自身がログを確認できない環境でも、この情報だけは残せます。
実務例:保存成功なのに再読み込みすると元に戻る
ここまでの内容を、一つのケースで整理します。
事象
ユーザー編集画面で部署を「営業部」から「開発部」に変更。
「保存しました」と表示されるが、再読み込みすると「営業部」に戻る。
① Request
PUT /api/users/123
{
"departmentId": 20
}
仕様上20 = 開発部であることを確認済み。
Requestには変更後の値が正しく入っています。
② Response
HTTP 200
{
"success": true
}
APIは成功として返しています。
③ DB
SELECT
id,
department_id,
updated_at
FROM users
WHERE id = 123;
結果はdepartment_id = 20。
DBにも変更後の値が保存されています。
④ 再取得API
画面再読み込み時には、次のAPIが呼ばれていました。
GET /api/users/123
Responseは次の内容です。
{
"id": 123,
"departmentId": 10,
"departmentName": "営業部"
}
ここで初めて差分が見つかりました。
PUT Request :20
PUT Response :success
DB :20
GET Response :10
UI :営業部
この時点でどう報告するか
確認できた事実として、次のところまで整理します。
- 更新Requestには
departmentId=20が送信されている - 更新APIは
success=trueを返している - DBの
department_idも20に更新されている - 再読み込み時の
GET /api/users/123ではdepartmentId=10が返却されている - 画面にはGET Responseどおりの「営業部」が表示されている
そのうえで、現時点の仮説として、
更新処理ではなく、ユーザー情報の取得処理側で異なる部署情報を取得している可能性があります。
と書きます。
ここで、
GET APIのJOINが間違っています。
とまでは書きません。
実際にJOINが原因なのか、Cacheなのか、別テーブルなのかは、この時点ではまだ確認できていないからです。
QAはどこまで調べればいいのか
調査を始めると、どこまで追えばいいのか迷うことがあります。
個人的には、次に見るべき場所を開発者が判断できるところまでを一つの目安にしています。
例えば、
Request :正常
Response :正常
DB :正常
GET Response :不正
UI :GET Responseどおり
まで分かっていれば、調査範囲はかなり狭まっています。
そこから先の、
- Repository
- SQL
- Cache
- Domain Logic
- 外部サービス
- サーバー内部の例外
まで、QAが必ず特定する必要はないと思っています。
もちろん、コードやログを見られる環境のQAであれば、さらに追うこともできます。
ただ、原因を最後まで当てることよりも、確認できた事実を正確に残し、未確認部分を明確にすることのほうが、個人的には価値が大きいと感じています。
私がバグ報告に残している情報
API関連の不具合では、必要に応じて次の情報を残しています。
事象
- 再現手順
- 期待結果 / 実際結果
- 環境
- User / Role
- 対象データID
- 再現日時
Request・Response
- URL / Method
- Path / Query Parameter
- Payload
- Status Code
- Response Body
DB
- 対象レコード
- 更新前 / 更新後
updated_at- Environment / Tenant
調査結果
- 確認できた事実
- 現時点の仮説
- 未確認事項
とはいえ、全部を毎回埋めているわけではありません。
単純な表示崩れにDB確認は不要ですし、Requestだけで原因箇所が十分絞れているなら、そこで止めることもあります。
調査項目を増やすことより、不具合に応じて必要な情報を取ることを意識しています。
まとめ
以前は、
500ならBE
RequestがおかしいならFE
DBがおかしいならBE
くらいの単純な図式で考えてしまうこともありました。
実際には、そこまで単純ではありません。
今は、
画面
↓
Request
↓
Response
↓
DB
↓
再取得
↓
画面
をつなげて見ながら、
- どこまでは期待どおりだったか
- 最初に差分が見つかったのはどこか
を整理するようにしています。
バグ報告では、事実・仮説・未確認事項を分けて書く。
QAが原因を100%特定できなくても、開発者が次に見るべき場所を絞れるところまで整理できれば、それで十分価値のある調査になると思っています。
APIやDBを見る目的は「QAが開発者の代わりにデバッグすること」ではなく、不具合が発生している境界をできるだけ狭くすることだと考えています。