HB DevTools

Knowledge

Playwright の page.route は Server Action 内の外部 API 呼び出しを傍受できない

page.route はブラウザ発リクエストのみ対象のため、Server Action 内で行う Gemini API 等の呼び出しはモック不可。UI 状態テストへの限定とローカルモックサーバーの2つの対処法を解説します。

PlaywrightE2EテストServer Actions

1. 発生した現象

Markdown Formatter の E2E テストで、MD変換ボタンをクリックした際の Gemini API 呼び出しをモックしようとして以下のコードを書きました。

await page.route('https://generativelanguage.googleapis.com/**', async (route) => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ candidates: [{ content: { parts: [{ text: '# Mock Result' }] } }] }),
  })
})

しかし、MD変換ボタンをクリックしてもモックレスポンスが返らず、 実際の Gemini API への呼び出しが走り続けました。page.route によるインターセプトが一切機能していませんでした。

2. 原因:page.route はブラウザ発リクエストのみ傍受できる

Playwright の page.route は、ブラウザ(クライアントサイド)から発信される HTTP リクエストのみを傍受します。

MD変換の Gemini API 呼び出しは app/tools/markdown-formatter/_actions/gemini-format.ts の Server Action('use server')内で行われます。 Server Action は Next.js サーバー(本プロジェクトでは Cloudflare Workers)上で実行されるため、 HTTP リクエストはサーバーサイドから Gemini API に直接送信されます。

ブラウザから見ると「MD変換ボタンをクリック → Next.js サーバーへ POST → サーバー内部で Gemini API 呼び出し」 という流れになります。ブラウザは Gemini API への直接リクエストを送信しないため、page.route には届かないのです。

// ブラウザから Next.js サーバーへのリクエスト(page.route で傍受できる)
POST /tools/markdown-formatter  ← ここは傍受できる

// Next.js サーバーから Gemini API へのリクエスト(page.route で傍受できない)
POST https://generativelanguage.googleapis.com/...  ← ここは傍受できない

なお、POST /tools/markdown-formatter(Server Action の呼び出し自体)はpage.route で傍受できます。 レスポンスを遅延させてローディング状態を観察するテストには有効です。

3. 対処法 A:テスト範囲を UI 状態に限定する

Gemini API のレスポンス内容に依存しないテストシナリオに絞ることで、 外部 API のモックなしに E2E テストを成立させます。

  • ローディング状態のテスト:page.routePOST /tools/markdown-formatter の応答を遅延させ、 その間にボタンが「MD変換中…」に変わり非活性になることを確認する。
    await page.route('**/tools/markdown-formatter', async (route) => {
      if (route.request().method() === 'POST') {
        await new Promise<void>((resolve) => setTimeout(resolve, 3000))
        await route.continue()
      } else {
        await route.continue()
      }
    })
  • デフォルト状態のテスト: ページ初期表示時にエラーセクションが非表示であること、 ボタンが「上限到達」ではなく「MD変換」ラベルであることを確認する。
  • 入力に応じたボタン活性化テスト: テキスト入力後に「MD変換」ボタンが有効になることを確認する。

Gemini API のレスポンス内容(変換結果の正確さ等)は E2E テストの責任範囲外として割り切り、 Server Action の単体テストや手動テストで確認します。

4. 対処法 B:ローカル HTTP サーバーでモックする

サーバーサイドの外部 API 呼び出しをモックしたい場合は、 E2E テスト側でローカル HTTP サーバーを起動し、 Server Action が呼び出す URL をテスト用のサーバーに向ける方法があります。

本プロジェクトの SEO Analyzer(e2e/seo-analyzer.spec.ts)がこのパターンを実装しています。test.beforeAllhttp.createServer を使ったモックサーバーを起動し、 Server Action の fetch 先として環境変数経由でモック URL を渡します。

  • メリット: サーバーサイドの HTTP レスポンス内容を完全にコントロールできる。 エラーレスポンス(429・503 等)を意図的に返すテストが書ける。
  • デメリット: テストコードの複雑さが増す。 Server Action がモック可能なエンドポイントを参照するよう 実装側にも配慮が必要(ハードコードされた URL の場合は差し替えが困難)。

5. 使い分けの判断基準

2つの対処法の使い分けは以下の観点で判断します。

  • 検証したい内容がレスポンス内容に依存するか: 「変換結果の文字列が正しいか」「エラーコードに応じた UI 変化が正しいか」を E2E で確認したい場合は、対処法 B(ローカルモックサーバー)が必要です。 一方、ローディング状態・ボタン活性状態・デフォルト表示のような レスポンス内容によらない UI 確認は対処法 A で十分です。
  • 外部 API の呼び出し URL が差し替え可能か: Server Action がハードコードされた URL を参照している場合は 環境変数経由での差し替えが必要です。 差し替え機構のない実装に後からモックサーバーを当てるのは改修コストがかかります。
  • テストの保守コスト: 外部 API のレスポンス仕様が変わるたびにモックも更新が必要です。 レスポンス内容の検証は単体テストに任せ、 E2E は UI フローの確認に集中させる設計が保守しやすいです。

本プロジェクトでは Markdown Formatter の MD変換テストに対処法 A を採用しました。 AI の変換結果は毎回異なるため E2E での内容検証は不適切であり、 UI 状態(ローディング・ボタン活性・エラー表示なし)の確認に限定することが テスト設計として自然な選択でした。