この春、E2Eテストの裏側にあるAPI差分を見る実験を始めた
はじめに
この春、E2Eテストの見方を少し変える実験を始めました。
これまでは、E2Eテストというと「ユーザー操作が最後まで成功するか」を見るものだと考えていました。
ログインできるか。検索できるか。保存できるか。注文できるか。
画面上のフローが通れば、少なくともユーザー体験としては大きく壊れていない。もちろん、それは今でも重要です。
ただ、いくつかのアプリをバージョンアップしながら検証しているうちに、別の疑問が出てきました。
E2Eテストが緑でも、その裏側で発生しているAPIの挙動は変わっていないと言えるのか。
画面は同じように動いている。テストも通っている。HTTPステータスも 200。
それでも、APIレスポンスのフィールドが増えたり、消えたり、型が変わったり、同じ画面操作で呼ばれるエンドポイントが変わったりすることがあります。
UI がその差分を直接使っていなければ、E2Eテストは失敗しません。
しかし、その API を管理画面プラグイン、外部連携、レポート処理、社内ツールが使っているなら、その変化は無視できないかもしれません。
この春、自分はこの「E2Eテストの裏側」をもう少し真面目に見ることにしました。
始めたこと
始めたことはシンプルです。
同じUIフローを異なるバージョンで実行し、その裏側のAPI通信を記録して比較する。
イメージとしてはこうです。
version A を起動
↓
同じUIシナリオを実行
↓
ブラウザから発生したAPI通信を記録
version B を起動
↓
同じUIシナリオを実行
↓
ブラウザから発生したAPI通信を記録
2つの記録を比較
↓
API挙動の差分を分類する
ここで見たいのは、単に「API が成功したか」ではありません。
- 同じ操作で同じエンドポイントが呼ばれているか
- ステータスコードが変わっていないか
- レスポンスの構造が変わっていないか
- 重要そうなフィールドが増えたり消えたりしていないか
- 型や nullability が変わっていないか
- 追加のAPI呼び出しが発生していないか
つまり、E2Eテストを「画面操作の確認」だけでなく、実ユーザーフローを使ったAPI回帰テストの入口として使う、という発想です。
きっかけになった実験
きっかけは、Medusa という OSS のアップグレード検証でした。
v2.13.6 から v2.14.0 への小さなバージョンアップです。UI上は特に問題なく動いていました。E2Eシナリオも通っていました。画面を見ている限り、目立つ変化はありません。
しかし、同じUI操作で発生したAPI通信を比べると、かなり多くの差分が出ました。
もちろん、その多くはノイズです。
- ID
- タイムスタンプ
- seed data の順序
- ランダムな token
- 実行ごとに変わるURL
こうした差分は、回帰として扱うべきではありません。
一方で、ノイズではなさそうな差分もあります。
- レスポンスに新しいフィールドが追加された
- 以前あったフィールドが消えた
- オブジェクトの形が変わった
- 同じ操作なのに追加のリクエストが発生した
- 呼ばれるエンドポイントが変わった
このときに気づいたのは、E2Eテストの緑は「UIがまだ動く」ことを示しているだけで、裏側のAPIの挙動が変わっていないことまでは保証していないということです。
たとえば、あるレスポンスフィールドが増えても、UIがそのフィールドを使っていなければ画面は壊れません。
しかし、外部連携や管理画面拡張がそのレスポンスを読んでいるなら、同じ変化でも意味が変わります。
最初に作った小さな手順
最初から大きな仕組みを作るのではなく、まずは小さく始めました。
1. 対象フローを一つ選ぶ
最初は、ユーザーにとって意味があり、かつAPI通信が複数発生するフローを選びます。
たとえば、
- ログインする
- 商品カテゴリを作成する
- 注文詳細を開く
- 設定を変更して保存する
- 検索条件を変更する
ポイントは、APIを直接叩くのではなく、UI操作から自然に発生するAPI通信を見ることです。
2. 同じフローを2つのバージョンで実行する
比較対象は、リリース前後、アップグレード前後、あるいは2つのコミットです。
baseline: v2.13.6
target: v2.14.0
のように、意味のある単位で比較します。
ここで大事なのは、テストデータや初期状態をできるだけ揃えることです。状態が大きく違うと、API差分なのかデータ差分なのか分かりにくくなります。
3. ブラウザのネットワーク通信を記録する
Playwright を使うなら、HAR や request / response hook で記録できます。
ざっくりしたイメージはこうです。
page.on("response", async (response) => {
const request = response.request()
const url = response.url()
if (!url.includes("/admin/")) return
const status = response.status()
const method = request.method()
const body = await response.text().catch(() => "")
// method, url, status, body, scenario step などを保存する
})
実際には、認証情報や秘匿情報を保存しない、巨大なレスポンスをどう扱うか、バイナリを除外する、などの処理が必要です。
4. 動的フィールドを正規化する
そのまま比較すると、ノイズだらけになります。
まずは、実行ごとに変わる値を正規化します。
{
"id": "<id>",
"created_at": "<timestamp>",
"updated_at": "<timestamp>",
"token": "<redacted>"
}
最初から完璧な正規化を目指す必要はありません。むしろ、何度か比較して「これは毎回変わるだけだ」と分かったものを少しずつ除外していく方が現実的でした。
5. 差分を分類する
差分を全部同じ重さで扱うと、すぐに読めなくなります。
自分は、まず次のように分けて見るようにしました。
| 種類 | 例 | 扱い |
|---|---|---|
| ノイズ | ID、時刻、token | 原則除外 |
| 値の差分 | 件数、名前、並び順 | データ差分か要確認 |
| 構造差分 | フィールド追加/削除 | 要確認 |
| 型の差分 | string → number | 重要度高 |
| nullability | string → null | 重要度高 |
| status差分 | 200 → 400 | 重要度高 |
| endpoint差分 | URL変更、追加呼び出し | 要確認 |
この分類を入れるだけで、「大量のdiff」から「見るべきdiff」へかなり近づきます。
やってみて分かったこと
1. 最初のdiffはほとんどノイズ
最初にdiffを出すと、かなり派手に見えます。
でも、その多くは実行ごとに変わる値です。
ここで「やっぱり使えない」と判断するのは早いと思いました。API差分を見るには、ノイズ除去が前提です。
むしろ、ノイズを減らしたあとに残る構造差分や型差分の方が重要です。
2. UIが使っていないフィールドほど見落とされやすい
E2Eテストは、画面に表示されるものには比較的強いです。
しかし、画面が使っていないレスポンスフィールドには弱い。
APIレスポンスには、UIが直接表示しないフィールドが多く含まれています。拡張機能や外部連携はそこを使っているかもしれません。
この領域は、通常のE2Eテストではかなり見落とされやすいと感じました。
3. 差分をすぐにバグ扱いしない方がよい
API差分が見つかったからといって、それが必ずバグとは限りません。
フィールド追加は意図的かもしれません。エンドポイント変更も内部リファクタリングとして正しいかもしれません。呼び出し回数が増えたのも、キャッシュ戦略の変更かもしれません。
だから、最初から「壊れています」と言うのではなく、
このUIフローで、APIのレスポンス構造がこのように変わっています。
これは意図した変更ですか?
回帰テストとして固定した方がよい挙動ですか?
と聞ける形にするのが大事だと思いました。
4. OSSには「発見結果」ではなく「受け取りやすいテスト」を渡す
この実験は、OSSプロジェクトに対する回帰テスト提案にも使えると感じています。
ただし、見つけた差分をそのまま「このツールをCIに入れてください」と提案するのは重い。
OSSメンテナが受け入れやすいのは、多くの場合、既存のテストフレームワークに沿った小さな回帰テストです。
つまり、
外部でUI/API差分を発見する
↓
必要なら挙動を確認する
↓
そのプロジェクトの既存テスト体系で小さなテストを追加する
という流れの方が摩擦が少ない。
発見と証拠作りには外部の仕組みを使っても、上流に渡すコードはメンテナが所有しやすい形にする。この方が現実的だと感じています。
実際、この春の実験では Medusa に対して、外部の仕組みを導入してもらうのではなく、既存の medusaIntegrationTestRunner を使った小さなPRを出しました。
内容は、Admin stock location API の metadata レスポンスを作成時と取得時に確認するテストです。
PRでやったことは、ほぼこれだけです。
const metadata = {
internal_id: "LOC-789",
priority: "high",
}
const response = await api.post(
"/admin/stock-locations",
{
name: "Stock Location Metadata Test",
metadata,
address: {
address_1: "456 Test St",
country_code: "US",
},
},
adminHeaders
)
expect(response.data.stock_location).toEqual(
expect.objectContaining({
metadata,
})
)
これは派手な変更ではありません。
でも、自分にとっては大事な一歩でした。
「外部で観測したAPI差分」を「そのプロジェクトが所有できるネイティブな回帰テスト」に変換する、という流れを実際に試せたからです。
まだ難しいこと
もちろん、まだ課題は多いです。
動的データの扱い
ID、時刻、並び順、seed data、認証情報などをどこまで正規化するかは難しいです。
正規化しすぎると重要な差分を消してしまいます。正規化しなさすぎるとノイズだらけになります。
シナリオとAPIの対応付け
「どのUI操作でどのAPIが発生したか」を正しく対応付けるのも重要です。
単にAPI一覧を出すだけでは、どのユーザーフローに影響するのか分かりません。
差分の重要度判断
フィールド追加は安全なことも多いですが、常に無視してよいわけではありません。
削除、型変更、nullability変更は重要度が高い一方で、プロジェクトのAPI方針によって扱いが変わります。
最終的には、人間が判断できる情報量に整理する必要があります。
この春に始めてよかったこと
この実験を始めてよかったのは、E2Eテストを見る視点が少し変わったことです。
以前は、E2Eテストの結果を「画面が通るかどうか」として見ていました。
今は、E2Eテストを次のようにも見ています。
ユーザー操作を再現することで、実際のAPI挙動を観測する入口。
これは API テストを置き換えるものではありません。
OpenAPI、契約テスト、ユニットテスト、統合テストにはそれぞれ役割があります。
ただ、実際のUIフローから発生するAPI通信を見ることで、仕様書や単体テストだけでは見えにくい変化を拾えることがあります。
そして何より、議論の材料が具体的になります。
このバージョン差分で、
この画面操作をしたとき、
このAPIレスポンスの形が変わっています。
ここまで言えると、「なんとなく不安」ではなく、具体的な確認依頼や回帰テスト提案にできます。
もう一つよかったのは、テスト自動化を「作って終わり」ではなく、変更を説明可能にするための観測として見られるようになったことです。
すべての差分を止めたいわけではありません。
むしろ、意図した変更は意図した変更として通したい。
ただし、重要な変更は、誰かが気づける形で残っていてほしい。
その意味で、E2Eテストの裏側にあるAPI通信は、まだ十分に使われていない観測ポイントなのではないかと思っています。
これから試したいこと
次に試したいのは、次のあたりです。
- ノイズ除去ルールをシナリオごとに管理する
- API差分をUIステップ単位で表示する
- status / structure / type / value の差分を分けてレポートする
- 差分から、そのプロジェクトの既存テストに追加できる最小の回帰テストを考える
- OSSプロジェクトに対して、外部レポートではなくネイティブなテストコードとして貢献する
特に最後の点は重要だと思っています。
外部ツールで発見したことを、そのまま外部ツールの導入要求にしない。
そのプロジェクトが既に使っているテスト体系に合わせて、メンテナが所有できる小さなテストに変換する。
この橋渡しができると、UI/API差分の観測は単なるレポートではなく、実際の回帰テスト改善につながるはずです。
おわりに
この春、自分は「E2Eテストの裏側にあるAPI差分を見る」実験を始めました。
まだ荒い部分は多いですが、E2Eテストを単なる画面確認ではなく、実ユーザーフローに基づくAPI挙動の観測手段として見ると、かなり面白いです。
UIは通っている。
でもAPIの挙動は変わっている。
このギャップは、今後もっと重要になるのではないかと感じています。
同じように、E2Eテストの裏側のAPI挙動や、バージョンアップ時のレスポンス差分を見ている方がいれば、どんな方法で運用しているか聞いてみたいです。
タグ
テスト自動化 E2Eテスト APIテスト 回帰テスト Playwright QA
Discussion