> ## Documentation Index
> Fetch the complete documentation index at: https://lib.findy.co.jp/llms.txt
> Use this file to discover all available pages before exploring further.

# E2Eテストとは — 信頼できる小さなテストスイート

> デプロイされた環境に対して主要なユーザー動線を検証する、小さく信頼できるE2Eテストスイートを維持します。

## 概要

E2E（End to End）テストは、実際のブラウザからアプリケーションを操作し、フロントエンド・バックエンド・データベースを通したシステム全体でユーザーの動線が完了することを検証するテストです。

E2Eは、ユーザーが体験するのと同じ形でシステムを検証できる唯一のテスト層ですが、実行と維持のコストは最も高くつきます。そのため、主要な動線を覆う小さなスイートを、失敗が常に意味を持つ信頼性で維持します。

本ページでは、E2Eテストスイートを信頼できる状態に保つ原則、Playwrightで安定したテストを書くためのパターン、そして外部サービスとの境界の橋渡し方を解説します。テストコード全般の原則は[テストコード](/ja/development/testing)を参照してください。

## 原則

### すべてのケースではなく、主要な動線を覆う

E2Eテストはデプロイされた環境を必要とし、1本あたり数秒から数分かかり、ネットワークの状態・テストデータ・タイミングといったコード以外の要因でも失敗します。シナリオを追加するほどリリースのコストは上がるため、1本1本がそこに置かれる理由を持つ必要があります。

スイートは、壊れたら事業への影響が直ちに現れる動線に限定します。

<CardGroup cols={3}>
  <Card title="認証" icon="right-to-bracket">
    ログインと会員登録です。ここが壊れると、すべてのユーザーが締め出されます。
  </Card>

  <Card title="収益に直結する動線" icon="cart-shopping">
    決済や注文の確定など、コンバージョンと利益に直結する操作です。
  </Card>

  <Card title="主要画面のスモーク" icon="list-check">
    各画面が表示され、中心となる操作が完了することの確認です。
  </Card>
</CardGroup>

このレベルのカバレッジで、E2Eが捕まえるべき失敗を検知できます。それはデプロイの破損と、単体では正しい部品同士の連携の破損です。

### 詳細は下位の層で検証する

実際のブラウザと実際のバックエンドを必要としない検証は、より下位の層に置きます。バリデーションの分岐・条件による表示の切り替え・コンポーネント内部のロジックはコンポーネントテストの方が速く正確に検証でき、E2Eは部品が正しくつながっていることだけを確認すれば足ります。

新しいシナリオをE2Eに置くかどうかは、1つの問いで判断します。その壊れ方は、システム全体を動かして初めて見えるものかどうかです。コンポーネントテストで捕まえられるなら、コンポーネントテストを書き、E2Eテストスイートはそのままにします。

### E2Eはデプロイ後にリリースの確認として実行する

E2Eテストはデプロイされた環境を検証するものなので、プルリクエストのパイプラインではなく、デプロイの後に実行します。リリースのマージによってステージング環境や本番環境へデプロイされたあと、これからユーザーが使う環境で主要な動線が動くことをスイートが確認します。

実行するシナリオはステージング環境と本番環境で変えます。選び方は後述の運用のセクションで扱います。

確認が失敗したら、その結果をロールアウトの停止や切り戻しというリリースの判断につなげます。デプロイを戻しやすく保つ方法は[リリース](/ja/development/release-strategy)を参照してください。

この分担により、最も遅い層がプルリクエストのクリティカルパスから外れてフィードバックが速く保たれ、それでいてすべてのリリースがシステム全体の検証を受けます。この分担が収まるパイプラインの設計は[CI/CD](/ja/development/ci-cd)を参照してください。

### 不安定なテストは即座に直すか隔離する

欠陥がないのに断続的に失敗するテストは、偽陽性の発生源です。チームは失敗の内容を読む代わりに再実行するようになり、やがて本物の破損まで再実行して見逃します。失敗したE2Eの実行にはリリースを止める力が保たれている必要があるため、リリースパイプラインにおいてこれは致命的です。

テストが不安定になったら、気づいたその日のうちに修正するか隔離します。隔離するには、`test.fixme()`でテストをリリースの確認から外し、修正を課題として起票します。原因を調査している間も、スイートの残りが信頼できる状態に保たれます。

```typescript Playwright theme={null}
// 不安定なテストを、修正できるまでリリースの確認から外す
test.fixme("注文履歴に確定した注文が表示される", async ({ page }) => {
  // ...シナリオはコードとして残し、実行だけを止める
});
```

次のセクションのパターンは、不安定さの主な原因を防ぐものです。信頼の仕組みは[テストコード](/ja/development/testing)で述べたとおり、通ることも失敗することも信頼できる状態を保つことです。

## Playwrightでの実践パターン

最初の3つのパターンはE2Eが不安定になる主要な原因に対応し、あとの2つは画面と権限が増えてもスイートの維持コストを一定に保つためのものです。

### ユーザーが見つけるのと同じ方法で要素を特定する

CSSクラスやDOM構造は実装の詳細です。スタイリングやリファクタリングによって挙動と無関係に変わり、そのたびにテストが壊れます。これは偽陽性です。

要素は、ロール・アクセシブルネーム・ラベルという、ユーザーが知覚するもので特定します。

```typescript Playwright theme={null}
// ❌ マークアップの詳細に依存している（スタイルや構造の変更で壊れる）
await page.locator(".btn-primary").click();
await page.locator("#order-list > div:nth-child(2) a").click();

// ✅ ユーザーと同じように、ロールとアクセシブルネームで特定する
await page.getByRole("button", { name: "注文を確定" }).click();
await page.getByRole("link", { name: "注文履歴" }).click();
```

<Tip>
  安定したアクセシブルネームがない要素は、`getByTestId`に切り替える前に、ラベルや`aria-label`の追加を検討します。テストとアクセシビリティの両方が同時に改善します。
</Tip>

### 時間ではなく、状態を待つ

固定時間のスリープはシステムの所要時間の推測であり、その推測は両方向に外れます。短ければ、負荷がかかったときにシステムが正しいのにテストが失敗します。安全なほど長くすれば、すべての実行でスイートが遅くなります。

Playwrightのweb-first assertionは期待する状態が現れるまで自動でリトライするため、テストは必要な時間だけ待ちます。

```typescript Playwright theme={null}
// ❌ タイミングを推測している（負荷がかかると失敗し、それ以外では時間を無駄にする）
await page.waitForTimeout(3000);
expect(await page.locator(".order-row").count()).toBe(3);

// ✅ タイムアウトまで、状態が現れるのを自動で待つ
await expect(page.getByRole("row", { name: /注文番号/ })).toHaveCount(3);
```

### テストごとに専用のデータを用意する

他のテストが残した状態を読むテストは、連鎖的に失敗します。単独でも、並列でも、異なる順序でも実行できず、1本の破損が依存するテストを道連れにします。各テストが必要なアカウントとデータを自分で用意し、自分が作ったものだけを検証します。

```typescript Playwright theme={null}
// ❌ 別のテストが作った状態に依存している
// （単独実行・並列実行・順序の変更で失敗する）
test("前のテストの注文が表示される", async ({ page }) => {
  await signIn(page, sharedAccount);
  await expect(page.getByRole("row", { name: "注文 #1" })).toBeVisible();
});

// ✅ アカウントとデータを自分で用意し、どこでも実行できる
test("注文履歴に確定した注文が表示される", async ({ page }) => {
  const account = await prepareTestAccount();
  const order = await createTestOrder(account);
  await signIn(page, account);
  await page.getByRole("link", { name: "注文履歴" }).click();
  await expect(page.getByRole("row", { name: order.number })).toBeVisible();
});
```

テストデータは、テストデータであると分かる名前にします。固定のプレフィックスと、タイムスタンプなどの一意なサフィックスの組み合わせが定石です。

一意なサフィックスは並列実行同士の衝突を防ぎ、識別できるプレフィックスは残ってしまったデータを安全に掃除できるようにします。このデータをUIではなくテスト支援APIで用意する方法は、後述の外部サービスのセクションで扱います。

### 認証はセットアップで一度だけ行い、再利用する

すべてのテストが冒頭でUIからログインする構成は、遅いうえに失敗を1箇所に集中させます。ログイン画面が変わると、スイート内の全テストが一斉に失敗します。

Playwrightでは認証とシナリオを分離できます。setupプロジェクトが権限ごとに一度だけログインし、`storageState`で認証済みの状態をファイルに保存し、各テストはログイン済みの状態から始まります。

```typescript Playwright theme={null}
// ❌ すべてのテストが本題の前にUIからログインする
test.beforeEach(async ({ page }) => {
  await page.goto("/login");
  // ...テストのたびに認証情報を入力する
});

// ✅ setupプロジェクトが権限ごとに一度だけログインし、状態を保存する
import { test as setup } from "@playwright/test";

setup(`authenticate as ${role}`, async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("メールアドレス").fill(user.email);
  await page.getByLabel("パスワード").fill(user.password);
  await page.getByRole("button", { name: "ログイン" }).click();
  await page.waitForURL("/dashboard");
  await page.context().storageState({ path: authFile(role) });
});

// ✅ テストは保存済みの状態を再利用し、ログイン済みで始まる
test.use({ storageState: authFile("admin") });
```

テスト用アカウントは権限ごとに用意します。たとえば管理者・マネージャー・スタッフの3種です。ログインの動線そのものも、setupが毎回すべての権限で通すため、カバーされ続けます。

### 画面×権限のスモークはデータから生成する

原則で述べたスモーク確認、すなわち主要な画面すべてが各権限で表示されることの確認はマトリクスであり、1マスずつ手で書く方法では規模に耐えません。画面の一覧を、それぞれを閲覧できる権限とともにデータとして一度だけ定義し、ループでテストを生成します。

```typescript Playwright theme={null}
// 画面をデータとして定義する: パス・期待する見出し・許可された権限
const screens = [
  { path: "/reports", heading: "レポート", roles: ["admin", "manager"] },
  { path: "/settings/members", heading: "メンバー管理", roles: ["admin"] },
];

// 画面×権限のスモークテストを一覧から生成する
// （roleはこのスイートが実行する権限。specファイルごとに1つ固定する）
for (const { path, heading, roles } of screens) {
  test(`${heading} ${path}`, async ({ page }) => {
    await page.goto(path);
    const expected = roles.includes(role) ? heading : "ダッシュボード";
    await expect(
      page.getByRole("heading", { level: 1, name: expected })
    ).toBeVisible();
  });
}
```

同じ一覧が両方向を検証します。許可された権限では画面の見出しが表示され、許可されていない権限では保護されたコンテンツではなくフォールバック先に着地します。

画面や権限の追加は一覧への1行の変更で済み、誤った権限に画面が公開されるというアクセス制御の退行も、表示を確認するのと同じスイートで失敗します。

## 外部サービスとの連携

外部サービスのログイン・メール配信・2要素認証のように、動線がブラウザの外に出る箇所は、ブラウザテストだけでは操作できず、稼働中の外部サービスに依存すると実行が非決定的になります。

テスト環境ではこの境界を2つの仕組みで橋渡しします。バックエンドの先で外部サービスの代わりに応答するスタブAPIと、UIからは到達できない状態をテストから準備・観測するためのテスト支援APIです。

これは[テストコード](/ja/development/testing)の原則、すなわち外部境界をモックし検証対象のロジックはモックしないことのE2E版です。E2Eではバックエンド全体が検証対象のロジックなので、固定する境界はその先、システムがチームの制御下にないサービスを呼び出す縁にあります。

### 状態の準備と観測はテスト支援APIで行う

シナリオが必要とする状態には、UIからは作成も読み取りもできないものがあります。登録導線のための新しい組織、2要素認証のワンタイムパスワード、アプリケーションが送信したメールの本文などです。

これらはテスト環境のテスト支援エンドポイントとして公開します。テストデータの作成と後片付け・コードの発行・配信されたメールの読み取りを用意すれば、シナリオは手作業なしで境界を越えられます。

メールの到達のようにブラウザの外で起きる事象は、web-first assertionの守備範囲外です。そのため、状態を待つというパターンはここでは形を変えます。固定時間のスリープではなく、上限付きのポーリングで待ちます。

```typescript Playwright theme={null}
// シナリオが必要とするデータをテスト支援APIで作成する
// （runIdはタイムスタンプなど、実行ごとに一意な識別子）
const org = await createTestOrg({ name: `e2e-signup-${runId}` });

// ❌ 配信にかかる時間を推測している（遅いと失敗し、速いと時間を無駄にする）
await page.waitForTimeout(15000);
const mail = await fetchLatestMail({ to: org.email });

// ✅ 上限付きのポーリングでメールの到達を待ってから読む
await expect
  .poll(() => fetchLatestMail({ to: org.email }), { timeout: 30_000 })
  .not.toBeNull();
const mail = await fetchLatestMail({ to: org.email });
```

### スタブとテスト支援APIを本番に置かない

テスト支援エンドポイントは設計上強力です。アカウントを作成し、認証コードを発行し、メールを読み取ります。だからこそ、ユーザーが到達できる場所にあってはなりません。

<Warning>
  テスト支援エンドポイントにはすべて認証を必須にし、公開APIとは分離した名前空間で提供し、テスト環境でのみ有効にします。本番環境から到達できるテスト支援エンドポイントは、セキュリティインシデントの予備軍です。
</Warning>

同じ隔離はシナリオにも適用します。これらのエンドポイントでデータを作る動線は、本番環境ではなくステージング環境に対して実行します。その制限の実装方法は、後述の運用のセクションで扱います。

### スタブと実サービスの乖離に注意する

スタブは外部サービスの契約の複製であり、複製は乖離します。提供元がレスポンスの形や認証フローを変えても、スタブは古い形で応答し続け、実際の連携は壊れているのにスイートは通ります。これは偽陰性です。

寛容すぎるスタブは、連携テストが防ぐべき失敗をちょうど隠してしまいます。

2つの防御を組み合わせます。まず、スタブは薄く保ちます。アプリケーションが実際に読むエンドポイントとフィールドだけを実装すれば、乖離しうる面積が減ります。

そして、実際の連携を通すシナリオを少数、外部サービスの実アカウントを安全に使えるステージング環境で実行し続けます。実連携のシナリオが乖離を捕まえ、スタブがスイートの残りを決定的に保ちます。

## リリースパイプラインでの運用

### 環境ごとに実行するシナリオを選ぶ

同じスイートをすべての環境でフル実行する必要はありません。データを作るシナリオや外部サービスを呼ぶシナリオはステージング環境に対して実行し、本番環境では読み取り中心のスモーク、すなわち画面が表示され副作用のない動線が完了することを確認します。

この制限はパイプラインの設定ではなくテスト自体に書き込みます。パイプラインが変わってもルールが生き残るためです。

```typescript Playwright theme={null}
test.skip(
  process.env.E2E_ENV === "production",
  "データを作成するシナリオは本番環境に対して実行しない"
);
```

### 失敗はtraceで調査する

デプロイされた環境での失敗は手元で再現しにくいため、証拠は実行時に採取します。Playwrightのtraceは、各操作をスクリーンショット・コンソール出力・ネットワーク活動とともに記録します。失敗した実行のtraceを保存するようパイプラインを設定すれば、失敗した実行は再現に苦しむ謎ではなく、見返せる記録になります。

長いシナリオを`test.step`で段階に分けておくことも、ここで効きます。traceがどの行ではなくどの段階で失敗したかを示すようになります。

### リトライで通ったら失敗のシグナルとして扱う

自動リトライは、インフラの一時的な不調と持続的な破損を区別でき、デプロイされた環境に対しては有効にする価値があります。

ただし、リトライで通ったテストは成功ではありません。それは不安定なテストであり、修正リストに載せます。リトライ後の成功を成功として数えると、スイートの信頼を損なう不安定さそのものが隠れてしまいます。

### リリースを判定できる速さを保つ

スイートはすべてのリリースを守るため、その実行時間はリリースのクリティカルパスに載ります。テストごとに独立したデータを持たせて安全に並列実行し、スイートの規模を意識的に抑えます。新しいシナリオが来たら、既存のシナリオが同じ動線を覆っていないか、下位の層で検証できないかを先に確認します。

## 関連ページ

<CardGroup cols={3}>
  <Card title="テストコード" icon="vial" href="/ja/development/testing">
    守るテストの原則。通ることと失敗することの信頼性、境界だけをモックすること。
  </Card>

  <Card title="CI/CD" icon="arrows-rotate" href="/ja/development/ci-cd">
    各テスト層を実行するパイプラインと、E2Eの起点になる自動デプロイ。
  </Card>

  <Card title="リリース" icon="rocket" href="/ja/development/release-strategy">
    デプロイとリリースの分離と、リリースの確認が守るもの。
  </Card>
</CardGroup>
