12k
All articles

インテグレーションテスト vs エンドツーエンドテスト

Webアプリの統合テストとE2Eテストを比較し、定義・JSツール・Vitest、MSW、Playwright、Cypressの使い分けを整理します。

OpenReplay Team
OpenReplay Team
インテグレーションテスト vs エンドツーエンドテスト

インテグレーションテストは、自分自身のコンポーネントやモジュールが連携して動作することを検証します。CIでミリ秒単位で実行され、外部システムはモックします。一方、エンドツーエンドテストは、実際のUIを通じてユーザーのように動作するアプリ全体を駆動し、デプロイ済みまたはステージングビルドに対して実行され、何もモックしません。この1つの違いが混乱のほとんどを解消しますが、フロントエンド開発に特有の第2の問題が潜んでいます。「インテグレーション」という言葉は、JavaScriptデベロッパーとバックエンドのQAエンジニアとでは意味が異なり、ブラウザにおける「インテグレーション」と「E2E」の境界線は実際のところ曖昧です。

本記事では、Webスタックの観点からその境界線を明確にします。具体的な例を交えた明確な定義、実際の判断基準に基づく比較、ユニットテストとの位置づけ、それぞれに対応するJavaScriptツール(Vitest、Jest、Testing Library、MSW、Playwright、Cypress)、そしてテスト予算の配分に関する明確な推奨方針を提示します。「両方使いましょう」という曖昧な答えではなく、具体的な指針を示します。

重要なポイント

  • インテグレーションテストは、ネットワークをモックした状態で自分自身のモジュールを組み合わせてレンダリングし、CIでミリ秒単位で実行されます。エンドツーエンドテストは、デプロイ済みのアプリを実際のブラウザで駆動し、何もモックしません。
  • JavaScriptアプリにおける「インテグレーション」は通常、ViTestまたはJest + Testing Library + MSWを意味し、「エンドツーエンド」は通常、実際のURLに対してPlaywrightまたはCypressを使用することを意味します。
  • フロントエンドにおけるインテグレーションとE2Eの境界線はスペクトラムであり、壁ではありません。どこに線を引くかは予算上の判断であり、絶対的なルールではありません。
  • コストに対する信頼性を最大化するために、主にインテグレーションテストを書き、最も重要なユーザージャーニーに対して3〜5件程度のE2Eテストを少数維持し、すべてをE2Eでカバーしようとしないでください。
  • 本番環境のバグは、定義上、テストで一度も検証されなかったジャーニーです。そのため、実際の障害のセッションリプレイが、不足しているテストを特定する最短経路となります。

インテグレーションテストとエンドツーエンドテストの概要

この2種類のテストは、予算配分に関わるあらゆる軸で異なります。カバー範囲、実行速度、実行場所、フェイク(モック)の対象、誤検知による失敗の頻度、そして各テストにしか検出できないバグの種類です。

基準インテグレーションテストエンドツーエンド(E2E)テスト
スコープ自分自身の複数モジュールの組み合わせ(例:コンポーネント + データレイヤー)フロントからバックまで、動作するアプリ全体
速度ミリ秒〜数秒テストあたり数秒〜数分
実行場所ローカルおよびCI環境、Nodeまたはjsdomで実行デプロイ済みまたはステージングビルドに対して、実際のブラウザで実行
モック対象外部システム(ネットワーク、サードパーティAPI)なし(または可能な限り少なく)
フレーキーさ低い(実際のネットワークや実際のブラウザタイミングに依存しない)高い(システム全体が安定していることに依存する)
コスト/メンテナンス作成・維持のコストが低い高い。アプリの変更に伴いスイートが劣化しやすい
固有の検出対象APIコントラクトの破損、レスポンスの形式不正、状態のワイヤリング認証、リダイレクト、環境設定、サードパーティスクリプト、ページをまたいだ状態

インテグレーションテストとは何か:Webの例を交えて

インテグレーションテストは、自分自身のコンポーネントのツリーをまとめてレンダリングし、ネットワークをスタブアウトした状態で組み合わせた動作を検証します。すでにユニットテスト済みのユニットが実際に連携して動作するかを確認するものです。ブラウザを起動せず、実際のサーバーにもアクセスしません。

具体的な例として、認証フックとプロフィール取得機能に接続されたLoginFormコンポーネントを考えます。インテグレーションテストでは、フォームをレンダリングし、フィールドに入力して送信し、ウェルカム状態が表示されることを検証します。ただし、POST /api/loginGET /api/profileの呼び出しはインターセプトされ、モックによって応答されます。テストの対象は、フォーム、フック、リクエストレイヤー、レンダリング結果の間のワイヤリングです。実際のバックエンド、認証プロバイダー、CDNはテストしません。

コントラクトのバグが表面化するのはこのレイヤーです。コンポーネントがuser.nameを期待しているのにハンドラーがuser.fullNameを返す、401パスでエラーバナーが再レンダリングされない、ローディングフラグがクリアされないといったケースです。インテグレーションテストは、モジュール間に潜むバグ(APIコントラクトの破損、レスポンスの形式不正、状態のワイヤリング)を、実際のデプロイのコストをかけずに検出します。

エンドツーエンドテストとは何か:Webの例を交えて

エンドツーエンドテストは、実際にデプロイされたアプリケーションを実際のブラウザで、ユーザーとまったく同じように操作し、ユーザーが目にするものを検証します。何もモックしません。実際のサーバー、実際のデータベース、実際の認証フロー、実際のリダイレクト、ページが読み込むサードパーティスクリプトをすべて実行します。

典型的な例は、ユーザーがログインしてチェックアウトするフローです。ライブURLにアクセスし、認証情報を入力して送信し、ダッシュボードに遷移し、カートにアイテムを追加し、支払いを完了して注文を確認します。フロントエンドバンドル、ネットワーク、API、データベース、セッションクッキー、決済連携など、すべてのレイヤーが関与します。

E2Eテストは、実際のデプロイ環境でしか存在しないバグを検出します。認証とリダイレクト、環境設定、サードパーティスクリプト、Content-Security-Policy違反、ページをまたいで保持されるクロスページ状態などです。これらはネットワークをモックした場合には再現できません。だからこそ、収益を生むジャーニーに対してE2Eはそのコストに見合う価値があるのです。

位置づけ:ピラミッドとトロフィー

どちらのテストタイプも、単一の関数やコンポーネントを独立して検証するユニットテストの上位に位置します。古典的なテストピラミッドは、基盤に多数のユニットテスト、中間に少数のインテグレーションテスト、頂点に極少数の低速E2Eテストを配置することを規定しています。スイートを高速に保つためのデフォルトとして合理的なアプローチです。

現代的な対案として、Kent C. Doddsが広めたテストトロフィーがあります。これはインテグレーションレイヤーを厚くします。Webアプリケーションにおいて、インテグレーションテストがコストに対して最も高い信頼性を提供するからです。実際の使用に近く、フルE2Eのフレーキーさや実行時間を伴いません。この考え方は、Testing Libraryの指導原則「テストがソフトウェアの使われ方に似ていれば似ているほど、より高い信頼性を与えられる」に基づいています。実際のコンポーネントツリーをレンダリングしてユーザーが目にする動作を検証するインテグレーションテストは、独立したリデューサーのユニットテストよりもはるかに実際の使用に近く、しかもミリ秒単位で実行されます。トロフィーはWebアプリにおける広く採用された現代的な見解として捉えてください。ピラミッドの普遍的な代替品ではありません。

フロントエンドにおける曖昧な境界線

フロントエンド開発において、インテグレーションとE2Eの境界線はスペクトラムであり、壁ではありません。コンポーネントのサブツリーをレンダリングするTesting Libraryのテストは「インテグレーション」であり、実際のURLを読み込むPlaywrightのテストは「E2E」です。どこに線を引くかは予算上の判断であり、絶対的なルールではありません。この曖昧な境界線こそが混乱の最大の原因です。同じ言葉が場所によって異なる意味を持つからです。

バックエンドQAの文献では、インテグレーションテストをボトムアップトップダウンビッグバンの戦略に分類しています。この分類は実在しますが、サーバーインテグレーションの専門用語であり、ViTestとPlaywrightで作業している人にはほとんど役立ちません。無視して構いません。Webデベロッパーにとって重要なのは実践的なマッピングです。「インテグレーション」はほぼ常にネットワークをモックしたコンポーネントツリーのレンダリングを意味し、「E2E」はほぼ常に実際のブラウザでデプロイ済みアプリを駆動することを意味します。中間のグレーゾーン(例えば、MSWでネットワークをインターセプトしながらローカル開発サーバーに向けたPlaywrightテスト)も問題ありません。ただし、どちらの予算から出すかを意識的に決定してください。

各テストタイプのJSツールチェーンとコード例

JavaScriptアプリにおける「インテグレーションテスト」は通常、ネットワークをモックした状態でコンポーネントツリーをレンダリングしてその動作を検証することを意味します。一般的にはVitestまたはJest + Testing Library + MSWの組み合わせです。「エンドツーエンドテスト」は通常、PlaywrightまたはCypressを使用してデプロイ済みアプリを実際のブラウザで駆動することを意味します。

Mock Service Worker(MSW)は、実際のバックエンドなしでインテグレーションテストをリアルにする重要なピースです。両方の環境でネットワークレイヤーのリクエストをインターセプトします。ブラウザではService Worker APIを介して(setupWorker)、Nodeでは低レベルのリクエストインターセプションを介して(setupServer、ViTestまたはJestのテストで使用し、service-workerファイルは不要)動作します。インターセプションはfetchをパッチするのではなく境界で行われるため、同じハンドラーをインテグレーションテスト、開発サーバー、さらにはPlaywrightの実行にも使用できます。ドキュメントには「開発、インテグレーション、エンドツーエンドテスト、そしてStorybook上で同じAPIモックを使用することを想像してください」と明記されています。

以下は、同じログイン&プロフィール取得機能のインテグレーションテストです。v1のres/ctxパターンを置き換えたMSW v2のhttp + HttpResponse APIを使用しています。

// login.integration.test.tsx — Vitest 4 + @testing-library/react 16 + MSW 2
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { LoginForm } from './LoginForm'

const server = setupServer(
  http.post('/api/login', () =>
    HttpResponse.json({ token: 'abc' })
  ),
  http.get('/api/profile', () =>
    HttpResponse.json({ name: 'Ada' })
  )
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

test('logs in and shows the profile name', async () => {
  render(<LoginForm />)
  await userEvent.type(screen.getByLabelText(/email/i), 'ada@example.com')
  await userEvent.type(screen.getByLabelText(/password/i), 'hunter2')
  await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
  expect(await screen.findByText(/welcome, ada/i)).toBeInTheDocument()
})

ブラウザは起動せず、サーバーも動作せず、ネットワークは完全に制御されています。そのため、このテストは高速で決定論的です。同じ機能をE2Eテストとして実装すると、デプロイ済みアプリに対して実行され、何もモックしません。PlaywrightはChromium、Firefox、WebKitを1つのAPIで駆動し、自動待機とWeb優先のアサーションを提供します。

// login.e2e.spec.ts — Playwright 1.6x
import { test, expect } from '@playwright/test'

test('user logs in and sees their profile', async ({ page }) => {
  await page.goto('/login')
  await page.getByLabel('Email').fill('ada@example.com')
  await page.getByLabel('Password').fill('hunter2')
  await page.getByRole('button', { name: 'Sign in' }).click()
  await expect(
    page.getByRole('heading', { name: /welcome, ada/i })
  ).toBeVisible()
})

アサーションは似ていますが、その背後にあるものはまったく異なります。Playwrightのspecは、実際の認証エンドポイント、リダイレクト、セッションクッキーが実際のブラウザで機能することを証明します。そしてその証明のために、実行時間とフレーキーさというコストを支払います。知っておくべき実践的な制限として、非同期のReact Server Componentsは新しいため、JestはそれらをサポートしておらずNext.jsは非同期コンポーネントにE2Eテストを推奨しています。これはインテグレーションレイヤーがまだ対応できず、E2Eがその役割を果たす具体的なケースです。

シナリオ別の判断ルール

習慣ではなく、防ごうとしているバグに基づいてテストタイプを選択してください。ルールは以下の通りです。

  • 新しいモジュールの連携またはフロントエンドとバックエンドのコントラクト → インテグレーションテスト。コンポーネントを新しいエンドポイントに接続する、新しいリデューサーを新しいセレクターに接続する、フォームをバリデーションレイヤーに接続する場合は、ツリーをレンダリングし、MSWでネットワークをモックし、動作を検証します。
  • スタック全体にわたる重要なユーザージャーニー → E2Eテスト1件。ログイン、サインアップ、チェックアウト、壊れると金銭的損失や信頼の喪失につながる1つのフロー。
  • それ以外のすべて → インテグレーションに頼り、E2Eにしないでください。

Webチームへの実践的な推奨事項:コストに対する信頼性を最大化するために主にインテグレーションテストを書き、最も重要なジャーニーに対して3〜5件程度のエンドツーエンドテストを少数維持し、すべてをE2Eでカバーしようとしないでください。ツールチップのE2Eテストはメンテナンスの負債ですが、インテグレーションテストは低コストの保険です。

E2Eスイートが劣化する理由と安定させる方法

エンドツーエンドスイートが劣化するのは、すべてのテストがシステム全体の安定性に依存しているからです。対策は、テスト数を少なく保ち、実装の詳細ではなくユーザーが目にする結果に対してアサートし、収益を守らなくなったテストを削除することです。テストIDの変更、ステージングデプロイの遅延、フレーキーなサードパーティウィジェット、リダイレクトの変更など、実際のリグレッションがなくてもパスしていたスイートを失敗させる可能性があります。そして、誤警報を出し続けるスイートは無視されるようになります。具体的には以下を実践してください。

  • 壊れやすいCSSパスではなく、ロールやラベルベースのセレクターを優先する
  • 固定のスリープではなくPlaywrightの自動待機に頼る
  • テストデータを分離する
  • スイートが信頼できる速さを保てるよう、テスト数を上限に抑える

しかし、どんなスイートも想定外のジャーニーはカバーできません。本番環境のバグは、定義上、インテグレーションテストとE2Eテストで一度も検証されなかったパスです。セッションリプレイがその真価を発揮するのはまさにここです。実際の障害のリプレイは、失敗した瞬間のDOM状態、ネットワーク呼び出し、コンソールエラーとともに、テストされていなかった正確なジャーニーを示してくれます。ログインやチェックアウトフローのセッションリプレイは、環境固有のリダイレクトやCSPによってブロックされたサードパーティスクリプトなど、ローカルのテストでは一度も実行されなかった障害モードを頻繁に明らかにします。そのリプロダクションを不足しているテストに変えることがプロとしての対応です。ジャーニーレベルの障害は新しいPlaywrightのspecに、コンポーネントやコントラクトの障害はTesting Library + MSWの新しいインテグレーションテストに変換し、同じバグが再発しないようにします。

まとめ

本番環境で有効な分割は、主にインテグレーション、少数の高価値E2E、そして純粋なロジックのためのユニットテストという構成です。インテグレーションは実行時間あたりの信頼性が最も高く、E2Eは実際のデプロイ済みブラウザでしか存在しないバグがあるからです。次の機能のモジュール境界に対してViTestまたはJest、Testing Library、MSWでインテグレーションテストを書くことから始め、壊れることが許されない少数のジャーニーにPlaywrightを使用し、実際の本番障害から書き忘れたテストを学んでください。

よくある質問

PlaywrightやCypressのような単一のツールでインテグレーションテストとエンドツーエンドテストの両方を書けますか?

はい、ただし区別はツールではなく、テストのスコープの設定方法によって決まります。PlaywrightとCypressは、ネットワークをモックした状態でコンポーネントを独立してマウントできます。これは実質的にインテグレーションテストです。あるいは、デプロイ済みのURLを読み込んでスタック全体を実行することもできます。これはエンドツーエンドです。両方にコンポーネントテストモードが存在しますが、JavaScriptエコシステムでは、Testing Libraryを使用したViTestまたはJestがより一般的なインテグレーションの手段です。

ViTestまたはJestのインテグレーションテストでMSWのmockServiceWorker.jsファイルは必要ですか?

いいえ。mockServiceWorker.jsファイルは、Service Worker APIに依存するsetupWorkerを使用するブラウザパスにのみ必要です。NodeベースのインテグレーションテストはsetupServerを通じて実行され、service workerではなく低レベルのクラス拡張を使用してリクエストをインターセプトするため、生成されたファイルは不要です。workerスクリプトを生成するのは、ローカル開発やブラウザベースのテスト実行など、実際のブラウザでリクエストをモックする場合のみです。

インテグレーションテストではなくエンドツーエンドテストを書くべきタイミングはいつですか?

防ごうとしているバグが実際のデプロイ環境でしか存在しない場合にE2Eテストを書いてください。認証、リダイレクト、環境設定、セッションクッキー、サードパーティスクリプト、Content-Security-Policy違反、クロスページ状態などがその例です。これらはネットワークをモックした場合には再現できません。APIコントラクトの破損、レスポンスの形式不正、状態のワイヤリングなど、自分自身のモジュール間に存在するものはすべて、インテグレーションテストの方が高速で低コスト、かつフレーキーさが低いです。E2Eテストは最も重要な収益ジャーニーのために確保してください。

E2Eテストがローカルでは通るのにCIで失敗するのはなぜですか?

エンドツーエンドスイートはシステム全体が安定していることに依存しているため、ローカルとCI環境の違いが実際のリグレッションなしにテストを失敗させます。よくある原因として、自動待機の代わりに固定スリープを使用していること、ビルド間でずれる壊れやすいCSSセレクター、CIデプロイのタイミングの遅延、共有または非分離のテストデータ、フレーキーなサードパーティウィジェットなどがあります。ロールやラベルベースのセレクターを優先し、Playwrightの自動待機に頼り、実行ごとにテストデータを分離し、スイートを信頼できる規模に保ってください。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.