コンテンツにスキップ

Playwright でサーバーサイドのリクエストをモックする(Next.js、TanStack Start)

Next.js や TanStack Start のアプリでは、サーバーがページをレンダリングする間に API を呼び出します。Playwright のリクエストのモック機能には、そうした呼び出しはまったく見えません。以下ではその理由を説明し、Playwright テストでそれらを制御する 4 つの方法を比較します。Next.js テストモード、サーバー内の MSW、mockttp のようなフォワードプロキシ、そして test-proxy-recorder による実際のレスポンスの記録です。他のツールに関する事実は、2026-10-03 にそれぞれのドキュメントとソースで確認したものです。

page.route() がサーバーサイドのリクエストを捉えられない理由

Section titled “page.route() がサーバーサイドのリクエストを捉えられない理由”

Playwright の page.route() が対象とするのは「ページが行うネットワークリクエスト」です。一方、Server Component、ルートの loader、server function はアプリの Node.js プロセスの中で実行されます。その fetch は、そのプロセスから API へ直接送られます。リクエストはブラウザを一切通らないため、Playwright にはインターセプトするものがありません。page.route() は、ブラウザがアプリに送るページのリクエストと、返ってくる HTML は捉えます。しかし、その HTML を作るためにサーバーが行ったリクエストは捉えません。

HAR ファイルにも同じ制限があります。page.routeFromHAR() が応答するのは「ページ内で行われるネットワークリクエスト」だからです。

MSW の Playwright との比較も同じことを述べています。page.route() が影響するのは「起動されたブラウザ内のトラフィックであり、Node.js プロセスではない」というものです。Playwright チームのサーバーサイドのモックに関する記事は、page.route() はブラウザのリクエストには使えると述べたうえで、「サーバーサイドの HTTP 呼び出しには使えない」としています。組み込みのサーバーサイドモックを求める機能リクエスト microsoft/playwright#30766 は、2026-05-22 にその記事へのリンクを添えてクローズされました。

Next.js には、next/experimental/testmode/playwright に実験的な Playwright インテグレーションが含まれています。ドキュメントは Next.js リポジトリ内の README だけで、Next.js の Playwright ガイドには記載がありません。テストが next.onFetch() でハンドラーを登録すると、Next.js はサーバーの fetch 呼び出しをそのハンドラーに送ります:

// README から抜粋して短縮したもの
import { test, expect } from 'next/experimental/testmode/playwright';
test('/product/shoe', async ({ page, next }) => {
next.onFetch((request) => {
if (request.url === 'http://my-db/product/shoe') {
return new Response(JSON.stringify({ title: 'A shoe' }), {
headers: { 'Content-Type': 'application/json' },
});
}
return 'abort';
});
await page.goto('/product/shoe');
await expect(page.locator('body')).toHaveText(/Shoe/);
});

必要なもの:

  • next.config.js の experimental: { testProxy: true }。
  • next/experimental/testmode/playwright の defineConfig で作った playwright.config.ts と、同じモジュールから test をインポートするテスト。

内部では、各 Playwright ワーカーが小さなプロキシサーバーを起動します。フィクスチャはページのリクエストに Next-Test-Proxy-Port ヘッダー(そのプロキシ)と Next-Test-Data ヘッダー(テスト id)を追加します(next-fixture.ts)。testProxy が有効だと、サーバーはそれらのヘッダーを読み取り、サーバーサイドの各 fetch をそのプロキシに送ります。プロキシはリクエストをそのテストのハンドラーに渡します(fetch.ts)。ハンドラーはテスト id ごとに保持されるため、並列のテストが 1 つの Next.js サーバーを共有できます。どのハンドラーも応答しないサーバーの fetch は Proxy request aborted をスローします。代わりに、ハンドラーが 'continue' を返してリクエストを通すこともできます。next.onFetch() は、ページ自身が他のオリジンに対して行う fetch 呼び出しも受け取ります。

現状:

  • 実験的な機能です。インポートパスと設定キーの両方がそう示しており、README のタイトルも「Experimental test mode for Playwright」です。
  • 修正は今も続いています。最新の修正 vercel/next.js#96525 は 2026-08-13 にマージされました。
  • README では、next/experimental/testmode/playwright/msw を通じてテストごとの MSW ハンドラーも提供しています。MSW 3.0(2026-09-28 リリース)では、このエントリーポイントの読み込みに失敗します。MSW 3.0 がもう依存していない strict-event-emitter をインポートしているためです。また、3.0 で削除された MSW の handleRequest() も呼び出しています(MSW 3.0 のリリースノート、msw.ts)。MSW 2.15.0 では読み込めます。どちらの結果も Next.js 16.3.8 で、2026-10-03 に確認したものです。

MSW は Node.js では msw/node の setupServer でリクエストをインターセプトします(Node.js インテグレーション)。MSW のドキュメントには Next.js のガイドがありません。MSW の作者は App Router に関する issue をクローズした際、インテグレーションの参考として mswjs/examples#101 を挙げました(コメント)。このプルリクエストは 2024-01-22 からオープンのままです。この例では、固定のハンドラー一覧を使い、ルートレイアウトからインターセプターを起動します:

// app/layout.tsx(mswjs/examples#101 より)
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { server } = require('@/mocks/node'); // setupServer(...handlers)
server.listen();
}

ハンドラーは Next.js のプロセス内に置かれます。この例ではそのプロセスを Playwright の webServer で起動しますが、webServer は実行全体で 1 つのプロセスを動かします(Playwright チームの記事)。すべてのワーカーがその 1 つのサーバーにページを送るため、ワーカーが並列に動くとハンドラーの振る舞いが変わります:

  • テストは自身のワーカープロセスで実行されるため、そのサーバーに対して server.use() を呼べません。MSW には、別プロセスのハンドラーを変更する API がまだリリースされていません。その提案である mswjs/msw#1617 の setupRemoteServer は、2023-05-12 からオープンのままです。MSW の Playwright バインディングは、それがリリースされるまでは page.route() に頼ると説明しています(@msw/playwright)。
  • server.use() を呼ぶ API ルートのような独自の経路を追加すると、その変更はすべてのテストに及びます。server.use() で追加したハンドラーは「サーバーインスタンス上に残り続けます」。server boundary の記事は、1 つのサーバーに対する同時の上書きが「すべてのテストで共有されるグローバルな状態」になると説明しています。#1617 で MSW の作者は、実際には「固定のハンドラー一覧」を保つか、「テストケースごとにアプリのインスタンス」を起動するかのどちらかになると述べています。

実際には、共有された Next.js サーバー内の MSW は、並列に動くすべてのテストに同じレスポンスを返します。MSW の上にテストごとのハンドラーを加えるツールが 2 つあります。Next.js テストモードは MSW のハンドラーを受け付けます(MSW 2 の場合。上記を参照)。scenarist は、サーバーのコードが各 fetch で転送する x-scenarist-test-id ヘッダーをキーにしてシナリオを対応づけます(Next.js アダプター)。

mockttp のようなフォワードプロキシ

Section titled “mockttp のようなフォワードプロキシ”

Playwright チームは #30766 をクローズする際、Simon Knott の記事を案内しました。この記事では、HTTP と HTTPS のモックサーバー兼プロキシである mockttp を、サーバーから外へ出るトラフィックの前段に置きます。各テストは mocks.forPost(url).thenJson(200, body) のようなルールを追加します。

アプリのコードは変わりません。サーバーのプロセスは、外向きのリクエストをプロキシ経由で送る環境変数を付けて起動します:

  • HTTP_PROXY と HTTPS_PROXY にプロキシの URL を設定する。
  • NODE_USE_ENV_PROXY=1。これで Node 組み込みの fetch がそれらの変数を使います。Node.js は 24.0.0 と 22.21.0 でこれを追加し、活発に開発中(active development)の機能としています(Node.js のドキュメント)。
  • プロキシの CA 証明書を指す NODE_EXTRA_CA_CERTS。これで、mockttp が HTTPS ホスト用に生成する証明書をサーバーが信頼します。

プロキシは 1 つの Playwright ワーカーに属するため、記事ではワーカーフィクスチャからワーカーごとに 1 つずつ、空いているポートでアプリサーバーを起動します。Playwright の webServer は、どのワーカーも存在しないうちに実行全体で 1 つのプロセスを起動するため、使いません。記事の構成では、ルールのないリクエストは実際の API へそのまま通ります。記事は、モックの書き忘れに気づけるよう、代わりにエラーで応答することを勧めています。

Playwright チームはこのアプローチを「サーバーの言語やフレームワークに依存しない」と説明しています。記事自体は、Next.js や TanStack Start ではなく素の Node サーバーを使っています。

test-proxy-recorder で実際のレスポンスを記録する

Section titled “test-proxy-recorder で実際のレスポンスを記録する”

上の 3 つの方法では、すべてのレスポンスを自分で書きます。test-proxy-recorder は、ローカルでの実行中に実際の API が返したものを記録し、CI で再生します。

  • プロキシ: テスト実行のため、アプリと並べて test-proxy-recorder <target-url> を起動します。TEST_PROXY_RECORDER_ENABLED が設定されている間は、アプリの API ベース URL をプロキシに向けます。各プロキシは 1 つのバックエンド、つまり起動時に指定した <target-url> に転送します。
  • registerProxyFetch(): サーバーで 1 回呼び出すと、グローバルな fetch にパッチを当てます。現在のリクエストの x-test-rcrd-id ヘッダーを、すべての外向きのリクエストにコピーします。プロキシはこのヘッダーを読み取り、サーバーサイドの各呼び出しをそれぞれのテストの記録に振り分けます。TEST_PROXY_RECORDER_ENABLED が設定されていない限り、本番ではこの呼び出しは何もしません(no-op)。
  • テストごとの id: playwrightProxy.before(page, testInfo, mode) は、spec ファイル、その describe のタイトル、テストのタイトルからセッション id を組み立てます。その id をページのリクエストに x-test-rcrd-id として付けて送り、そのセッションを record または replay に切り替えます。並列のワーカーは 1 つのアプリサーバーと 1 つのプロキシを共有しますが、それでも各テストは専用の記録を持ちます。
app/layout.tsx
import { registerProxyFetch } from 'test-proxy-recorder/nextjs';
registerProxyFetch();
src/router.tsx
import { registerProxyFetch } from 'test-proxy-recorder/tanstack-start';
registerProxyFetch();
e2e/fixtures.ts
test.beforeEach(async ({ page }, testInfo) => {
await playwrightProxy.before(page, testInfo, 'replay', { url: /localhost:8100/ });
});

実際の API に対して一度記録し、.mock.json(サーバー)と .har(ブラウザ)のファイルをコミットします。その後、CI はバックエンドをオフにしたままそれらを再生します。再生時、記録のないリクエストにはそのリクエストを示す 404 が返され、API には到達しません。

記録したレスポンスがカバーするのは、記録中に API が実際に返したものだけです。API が思いどおりに返してくれないエラーやエッジケースを強制するには、上記の方法のいずれかで手書きのハンドラーを使う方が簡単です。

セットアップの詳細は Next.js と TanStack Start のガイドにあります。

方法アプリ側のセットアップ並列テストハンドラーか、記録したレスポンスかNext.js と TanStack Start
Next.js テストモードnext.config.js の experimental.testProxy対応。ハンドラーはテストごとに保持されるハンドラー(next.onFetch()、または MSW 2 のハンドラー)Next.js のみ
サーバー内の MSWサーバーのコードで起動する setupServer(MSW の例ではルートレイアウト)そのサーバー上のすべてのテストでハンドラーセットは 1 つハンドラーNext.js: オープンなプルリクエスト内の例。TanStack Start: 記載なし
mockttpコード変更なし。サーバーのプロセスにプロキシ用の環境変数と CA 証明書対応。ワーカーごとに 1 つのアプリサーバーハンドラー。ルールのないリクエストはそのまま通過記載なし。Playwright チームはフレームワークに依存しないとしている
test-proxy-recorderテスト中は API ベース URL をプロキシに向け、加えて registerProxyFetch()対応。共有のアプリサーバー 1 つで実際の API から記録どちらも記載あり

比較のページでは、Mocky Balboa、scenarist、Polly.js、talkback、proxay を含むより多くのツールと、ブラウザサイドの選択肢を扱っています。