> ## 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.

# テスト戦略とは — Testing Trophyで検証の層を決める

> フロントエンドのテストをTesting Trophyの4層に整理し、それぞれの検証をどの層に置くかを決めるためのテスト戦略を解説します。

## 概要

テスト戦略とは、どの検証をどの層とどのツールで行うかという一連の決定です。本ページでは、フロントエンドテストの層モデルと各層の責務、そして新しい検証を正しい層に置くための判断基準を解説します。

フロントエンドのコードには、不具合の出方が異なる関心事が混在しています。描画のロジック、ユーザーの操作、見た目、バックエンドとの通信です。これらすべての不具合を妥当なコストで検出できる単一のテストはないため、フロントエンドのテストスイートは責務の異なる複数の層を組み合わせて作ります。

層に共通する原則は1つです。すべての検証を、その失敗を検出できる最も低い層で行います。低い層ほど速く実行できて原因を正確に指し示し、高い層はシステムの広い範囲を動かしたときにしか現れない失敗のために残します。

本ページで扱うのは、それぞれの検証をどの層に置くかです。個々のテストを信頼できる状態（通ることも失敗することも信頼できる状態）に保つ方法は[テストコード](/ja/development/testing.md)を参照してください。

## Testing Trophy

**Testing Trophy**は、Kent C. Dodds氏が提唱したフロントエンドテストの層モデルです。テストをStatic・Unit・Integration・E2Eの4層に整理します。

> **Static**
>
> 型チェックとリントによる静的な解析です。コードを実行せずに、型の不一致や存在しない参照といった種類の欠陥をまとめて排除します。

> **Unit**
>
> 単一の関数やモジュールを分離して検証します。速く正確で、入力パターンの多い純粋なロジックに向きます。

> **Integration**
>
> バックエンドとの境界をモックし、描画とユーザー操作を通してコンポーネントを検証します。トロフィーで最も広い層です。

> **E2E**
>
> デプロイされた環境に対して実際のブラウザを操作し、システム全体を通して主要な動線が完了することを検証します。

4層を速度とコストで並べると次のとおりです。

| 層 | 実行速度 | 準備と維持のコスト | 失敗が指し示す場所 |
| - | - | - | - |
| Static | 最速（エディタで即時） | 最小 | 問題の行 |
| Unit | 速い | 小 | 関数と入力 |
| Integration | 速い（Unitより遅い） | 中（境界のモックが必要） | コンポーネント |
| E2E | 遅い | 大（デプロイされた環境が必要） | 動線のどこか |

各層では、コストと信頼度がトレードオフです。高い層ほどユーザーの体験に近い形でシステムを動かすため、1本のテストが通ったときに保証できる範囲は広くなります。その一方で、実行は遅く、準備は多く、失敗は原因から遠い場所で報告されます。

低い層は速く、安く、正確です。たとえば型チェックが失敗すれば、問題のある行がそのまま示されます。その代わり、個々のチェックは分離された小さな範囲しか見ていないため、部品同士を組み合わせたときに動くことは証明できません。

トロフィーの形は、フロントエンドでのこのバランスの取り方を表しています。Integration層のテストが最も多いのは、コンポーネントテストが描画、状態、操作をまとめて検証でき、しかもデプロイされた環境なしにプルリクエストのパイプラインで速く実行できるためです。

> [!TIP]
>
> 各層が独立して動作し、ある層が担う品質の劣化はその層のテストで検知できることが重要です。型の誤りを最初に検知するのがUnit Testであったり、関数が投げる例外を検知できるのがIntegration Testであったりしてはいけません。型の誤りはStatic、関数の例外はUnitというように、それぞれの層が異なるアプローチでプロダクトコードを検証することで、全体として堅牢な品質を支えます。

各層で使うツールは少数に決まっています。

| 層 | 主に検出する失敗 | ツール |
| - | - | - |
| Static | 型の不一致・存在しない参照・既知の欠陥パターン | TypeScript・ESLint |
| Unit | 関数やモジュールが誤った結果を返すこと | Vitest |
| Integration（振る舞い） | 操作に対して画面が誤って振る舞うこと | Vitest + Testing Library |
| Integration（見た目） | 意図しない見た目の変化 | Storybook + ビジュアルリグレッションテスト |
| E2E | フロントエンド・バックエンド・データベース間の連携の不具合 | Playwright |

低い層はプッシュのたびにプルリクエストのパイプラインで実行し、E2Eテストスイートはデプロイ後にリリースの確認として実行します。パイプラインの設計は[CI/CD](/ja/development/ci-cd.md)を、E2Eをデプロイ後に実行する理由は[E2Eテスト](/ja/frontend/e2e-testing.md)を参照してください。

### Static Test

Static Testは、コードを実行せずに行う静的な解析で、型チェックとリンターを指します。一度設定すればコードベース全体を毎回検証でき、検証1件ごとの追加コストがかからないため、トロフィーの最下層になります。

型チェックは、不正な状態を検出するのではなく排除するものです。型システムが拒否する呼び出しはコンパイルを通らないため、その値を1つずつ試す実行時テストは不要になります。不正な入力のテストを書く前に、型でその入力を不可能にできないかを確認します。

```typescript TypeScript theme={null}
// ❌ 型システムが拒否できる値を実行時テストで列挙している
test("未知のステータスで例外を投げる", () => {
  expect(() => statusLabel("shipped!")).toThrow();
});

// ✅ union型が不正な呼び出しをコンパイルエラーに変え、
// 実行時テストは不要になる
type OrderStatus = "pending" | "paid" | "shipped";

function statusLabel(status: OrderStatus): string {
  // ...
}

// statusLabel("shipped!"); // コンパイルエラー
```

リンターは、型では表せない既知の欠陥パターンを検出し、型チェックを補完します。たとえば、読み取っている値が依存配列に入っていないフックや、支援技術が解釈できないマークアップです。どちらの検査もエディタとプルリクエストのパイプラインで動くため、この層のフィードバックは全層の中で最も速く返ってきます。

この層では振る舞いを検証できません。型チェックを通る関数が誤った値を返すことはあるため、ここから上の層ではコードを実行する必要があります。

### Unit Test

Unit Testは、単一の関数やモジュールを分離して実行し、入力に対する出力を検証します。日付範囲の正規化や金額の計算のように、入力パターンの多い純粋なロジックが対象で、Vitestで書きます。

```typescript Vitest theme={null}
import { normalizeDateRange } from "./normalizeDateRange";

// 入力と出力だけで検証する。描画も操作も伴わない
test("開始日と終了日が逆なら入れ替える", () => {
  expect(
    normalizeDateRange({ from: "2026-10-09", to: "2026-10-01" })
  ).toEqual({ from: "2026-10-01", to: "2026-10-09" });
});
```

この層の強みは速さと正確さです。1つのテストが1つの関数しか動かさないため、失敗すればどの関数がどの入力で誤ったかがそのまま分かります。カスタムフックの戻り値もこの層で検証できますが、描画や操作を伴う検証はIntegrationに置きます。

この層では組み合わせを検証できません。関数が単体で正しくても、コンポーネントに組み込んだときに画面が正しく振る舞う保証はなく、それはIntegrationの役割です。

### Integration Test

Integration Testは、コンポーネントとフックを組み合わせ、ユーザーの視点に近い形で検証します。バックエンドとの境界はモックし、描画と操作の結果を画面から観測します。

この層の検証は2種類です。振る舞いの検証では、Testing Libraryでコンポーネントを描画し、クリックや入力を再現して画面の変化を確かめます。バックエンドの応答はテストプロバイダーやテストツールで差し替え、実際に近いデータを与えたときの表示まで検証します。見た目の検証では、Storybookのストーリーを描画し、ビジュアルリグレッションテストで意図しない差分を検出します。

```tsx Vitest + Testing Library theme={null}
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { OrderSearchForm } from "./OrderSearchForm";

// 画面を描画し、ユーザー操作への反応を観測する
test("不正な日付範囲ではエラーを表示し、検索を無効にする", async () => {
  const user = userEvent.setup();
  render(<OrderSearchForm />);

  await user.type(screen.getByLabelText("開始日"), "2026-10-09");
  await user.type(screen.getByLabelText("終了日"), "2026-10-01");

  expect(screen.getByRole("alert")).toHaveTextContent("終了日は開始日以降");
  expect(screen.getByRole("button", { name: "検索" })).toBeDisabled();
});
```

> [!NOTE]
>
> UnitとIntegrationは同じテストランナー（Vitest）で書くため、実際には区別されずに書かれることも多くあります。本ページでは、描画や操作を伴うかどうかで2つを区別します。

この層では実際のバックエンドとの連携を検証できません。応答をモックしている限り、実際のAPIの仕様が変わってもテストは通り続けます。

### E2E Test

E2E Testは、デプロイされた環境に実際のブラウザでアクセスし、ログインから結果の表示までの主要な動線がシステム全体を通して完了することを検証します。ツールはPlaywrightです。

```typescript Playwright theme={null}
import { test, expect } from "@playwright/test";

// ログイン状態はstorageStateで再利用し、UIからのログインは繰り返さない
test("ログイン済みのユーザーが注文を検索して結果を見られる", async ({ page }) => {
  await page.goto("/orders");
  await page.getByLabel("開始日").fill("2026-10-01");
  await page.getByLabel("終了日").fill("2026-10-09");
  await page.getByRole("button", { name: "検索" }).click();

  await expect(page.getByRole("table")).toContainText("注文 #");
});
```

フロントエンド、バックエンド、データベースの連携は、この層でしか検証できません。その一方で実行は遅く、失敗の原因は動線のどこにでもあり得るため、スイートは主要な動線に絞って小さく保ちます。

E2Eはデプロイ後にリリースの確認として実行します。スイートの設計とPlaywrightの実践パターンは[E2Eテスト](/ja/frontend/e2e-testing.md)を参照してください。

## 層の使い分け

検証を追加するときは、層を下から順に確かめ、その失敗を検出できる最初の層に置きます。判断の問いは次の4つです。

* 型かリントのルールで保証できるなら、Staticに置きます。
* 単一の関数の入出力として検証できるなら、Unitに置きます。
* バックエンドをモックしたうえで、画面の描画や操作から観測できるなら、Integrationに置きます。
* どれにも当てはまらず、システム全体を動かして初めて検証できるなら、E2Eに置きます。

この判断を注文検索フォームのような1つの画面に適用すると、検証は4層すべてに分かれます。

| 検証したいこと | 層 | 理由 |
| - | - | - |
| 検索パラメータの名前がAPIの仕様と一致している | Static | APIクライアントの型が誤記をコンパイル時に拒否する |
| 日付範囲が正しく正規化される | Unit | エッジケースの多い純粋な入出力である |
| 不正な範囲でエラーが表示され検索ボタンが無効になる | Integration | 描画と操作は必要だが、実際のバックエンドは不要である |
| ログインしたユーザーが検索して結果を見られる | E2E | フロントエンド・バックエンド・データベースがそろって初めて不具合が現れる |

同じ失敗を2つの層で検出できるときは、低い方を選びます。低い層のテストはデプロイ後を待たずにプッシュのたびに実行され、失敗が症状ではなく原因を指し示すためです。

この規則は、E2Eテストスイートを小さく保つことにもつながります。コンポーネントテストで検出できる失敗はE2Eに持ち込まず、E2Eにはシステム全体を通して検証する動線だけを残します。詳細は[E2Eテスト](/ja/frontend/e2e-testing.md)の原則を参照してください。

> [!NOTE]
>
> 検証を低い層に置いても、その上の層が不要になるわけではありません。たとえば、バックエンドをモックしたコンポーネントテストでは、実際のAPIの仕様と食い違っても検出できません。各層には、その層でしか検出できない失敗が残ります。

## 関連ページ

以下のページで、E2E層の詳細と守るテストの共通原則を扱います。

> **[E2Eテスト](/ja/frontend/e2e-testing.md)**
>
> 小さなE2Eテストスイートで主要な動線を守る原則と、Playwrightで安定させるパターン。

> **[テストコード](/ja/development/testing.md)**
>
> 守るテストの原則。通ることと失敗することの信頼性、境界だけをモックすること。

> **[CI/CD](/ja/development/ci-cd.md)**
>
> 各層が実行されるパイプライン。コミット時の検査からデプロイ後のE2Eまで。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.