zudo-test-wisdom
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

外部サービスのフェイクを置く境界の選び方

アプリケーションが実際に持つトランスポート境界を見極め、ネットワークインターセプトとアダプターフェイクを選択する。

ツールではなく境界から考える

外部 API に依存するフロントエンドには、外部サービスの代役が必要です。しかし「MSW を使う」だけでは、まだテスト設計上の判断になっていません。まず、テスト対象のコードを処理が離れる直前に、アプリケーションが越える最後の境界を特定します。通常の Web アプリなら、その境界は fetch かもしれません。デスクトップアプリなら、たまたま実装先が Rust である型付きブリッジかもしれません。フェイクは、その実在する境界に置きます。

アプリケーションの形実際の境界そこに置くフェイク実物のまま残るもの
ブラウザが REST または GraphQL を直接呼ぶHTTP リクエストMSW / ネットワークインターセプトfetch、URL 構築、ヘッダー、シリアライズ
フロントエンドがバックエンドブリッジを呼ぶアダプターインターフェースインメモリのアダプター実装UI、状態、ブリッジの呼び出し箇所、ドメインの振る舞い
トランスポートがネイティブ、IPC、ソケット、またはブラウザからインターセプト不能アダプターまたはプロトコルの抽象化その抽象化のフェイク実装トランスポートより上のすべて

これはライブラリの好みではなく、可視性の問題です。テストランナーから観測できない境界の下にフェイクを置いても機能しません。必要以上に上へ置けば、テストできたはずの本番コードを黙って取り除くことになります。

HTTP が境界ならネットワークインターセプトを選ぶ

REST または GraphQL を直接呼ぶ通常のブラウザアプリでは、MSW がたいてい最も狭く有用な置き換えになります。アプリケーションは本物の fetch を実行し、本番と同じコードで URL とクエリパラメーターを組み立て、ヘッダーとリクエストボディを書き、レスポンスをデシリアライズします。置き換わるのはリモートサーバーだけです。

この違いは重要です。/users が /user になった、JSON フィールドを誤った名前でシリアライズした、GraphQL の変数を渡し忘れた、といった不具合はインターセプトされるリクエストまで到達するため、テストで捕捉できます。ドメインオブジェクトを返すフェイクの JavaScript クライアントでは、どの不具合も通過してしまいます。これは Backend & Node.js Testing で述べている分離です。フロントエンド自身の HTTP クライアントが存在しないことにせず、バックエンドの稼働状況からフロントエンドを独立させられます。

Tip

HTTP を取り除くとテストしたい振る舞いまで失われるなら、MSW を使います。HTTP がアプリケーションにとって唯一の外部サービス境界なら、ネットワークインターセプトによって、ブラウザテストが確実に到達できる最も下流にフェイクを置けます。

HTTP をインターセプトできないならアダプターフェイクを選ぶ

次は、パッケージ化された Tauri アプリを考えます。レンダラーがバックエンドブリッジを呼び、ブリッジが Tauri の IPC チャンネルを越え、Rust がネイティブ HTTP 通信と SSE ストリームの受信を担います。ブラウザから見ると、インターセプトできるリクエストは存在しません。ネイティブ HTTP 通信も SSE フレームも、レンダラーの Service Worker を通らないため、MSW からはまったく見えません。

この場合、アダプター境界は最適化ではなく、アーキテクチャによって強制される選択です。フロントエンド側のバックエンドブリッジをインターフェースとして定義し、モック開発モードではパッケージ版の実装をステートフルなインメモリ実装へ差し替えます。メインアプリでは本物のコンポーネント、状態遷移、インターフェースの呼び出しが引き続き実行され、IPC の先から始まるはずの振る舞いだけをフェイクが提供します。Tauri アプリのテストでは基礎となる Backend Bridge Mock Adapter Pattern を、ステートフルなフェイクサービスでは完全なフェイクサービスのレシピを説明しています。

同じ選択は HTTP 以外のトランスポートにも適用できます。ネイティブソケット、ファイルシステムを介した同期、プラットフォーム固有チャンネル、あるいは通常のブラウザでは実装を利用できない任意のブリッジです。また、WebKit E2E でも Chromium と同一のテストモードを使えます。ランナーはモックアプリを起動してアダプターを呼びます。Service Worker の登録タイミング、スコープ、準備完了を前提としないため、WebKit の実行に登録処理由来の別種の不安定さを持ち込みません。

アダプターフェイクが迂回した層を別のテストで返済する

アダプターフェイクは意図的に、パッケージ版トランスポートより上で処理を止めます。これによりメインの E2E スイートは決定的になりますが、そのテストは Rust ネイティブの HTTP/SSE 実装について何も保証しません。この空白を、偶然カバレッジが足りない箇所ではなく、明示的な責務として扱います。

フェイクと組み合わせて、HTTP 境界に専用のトランスポートコントラクトテストを置きます。Tauri の例なら、少なくとも次の性質を証明できる深さまで、本物のトランスポートコードを駆動します。

  • リクエスト前に origin が正規化されること。

  • リダイレクト時に予期しない宛先へ権限を転送せず、fail-closed になること。

  • エラー、ログ、保存される診断情報から Bearer トークンが秘匿されること。

  • イベント境界、分割チャンク、終端条件をまたいで SSE フレームを正しく解析すること。

これらはトランスポートの性質です。アダプターフェイク内に再現しても、互いに同意する第2の実装を作るだけです。アダプターテストはブリッジより上のアプリケーション動作を、トランスポートコントラクトスイートはブリッジ実装とその HTTP/SSE 境界を担当します。

Warning

アダプターフェイクは、本番の接続性が機能する証拠ではありません。アダプタースイートが green でもトランスポートコントラクトスイートがなければ、そのアーキテクチャは不安定なテストを未テストの層と交換しただけです。

1つのアプリで両方の境界を使える

選択はリポジトリごとではなく、アーキテクチャ境界ごとに行います。Tauri 製品では、メインアプリの E2E スイート全体をステートフルなアダプターフェイクに対して実行し、Playwright から pnpm dev:mock のような明示的なモックモードを起動できます。パッケージ版の製品が最終的にはネイティブ IPC を介して通信するとしても、メインアプリの全シナリオは Chromium と WebKit で同じ制御可能なステートマシンを使います。

同じリポジトリに、HTTP API を直接呼ぶ管理画面のレンダラーが含まれる場合もあります。そのサブアプリでは HTTP こそが実際のアプリケーション境界なので、MSW が引き続き適切です。テストのためだけに作ったアダプターで隠すことなく、管理画面の URL 構築とシリアライズを実物のまま残せます。

見た目の一貫性のために、リポジトリ全体のモック手段を1つへ統一しないでください。統一するのは判断基準です。各サーフェスは本番コードが実際に越える境界をフェイクにし、フェイクによって隠れた層には別のコントラクトスイートを用意します。

要点:実際のアーキテクチャ境界でモックする

アプリケーションの実際のアーキテクチャ境界でモックします。その境界が HTTP なら MSW を使います。ブリッジの抽象化がすでに存在するか、本番トランスポートをインターセプトできないなら、アダプターフェイクを使います。どちらを選んでも、フェイクが迂回した層をコントラクトテストで検証します。

このルールなら、テストランナーから見えない通信の観測を求めることなく、有用な本番経路を最大限に残せます。同時に、残る死角も明確になります。ネットワークインターセプトでは実際のリモートサービスが、アダプターフェイクでは具体的なトランスポートがテストの外に残ります。どちらの置き換えも、対応するコントラクトスイートがあって初めて、根拠のあるテスト境界になります。

Revision History

作成更新