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

# What is a testing strategy? Choosing the right test layer

> Assign each verification to the lowest test layer that can catch its failure, using the Testing Trophy as the map of a frontend test suite.

## Overview

A testing strategy is the set of decisions about which test layer verifies what, and with which tool. This page explains the layer model for frontend testing, what each layer is responsible for, and the criteria for placing a new verification in the right layer.

Frontend code mixes concerns that break in different ways: rendering logic, user interaction, visual appearance, and communication with the backend. No single kind of test catches all of these failures at a reasonable cost, so a frontend suite combines several layers, each with a distinct responsibility.

One principle organizes the layers: verify everything at the lowest layer that can catch the failure. Lower layers run faster and point more precisely at the cause, and the higher layers stay reserved for failures that only appear when larger parts of the system run together.

This page covers where each verification belongs. How to make each verification strong, so that passing and failing both stay trustworthy, is covered in [Test Code](/development/testing.md).

## The Testing Trophy

The **Testing Trophy** is a layer model for frontend testing proposed by Kent C. Dodds. It arranges tests into four layers: static, unit, integration, and end-to-end (E2E).

> **Static**
>
> Type checking and linting analyze the code without running it, and reject whole classes of defects such as type mismatches and references that do not exist.

> **Unit**
>
> Verifies a single function or module in isolation. Fast and precise, suited to pure logic with many input patterns.

> **Integration**
>
> Verifies a component tree through rendering and user interaction, with the backend boundary mocked. The widest layer of the trophy.

> **E2E**
>
> Drives a real browser against a deployed environment and verifies that primary user journeys complete across the whole system.

Lined up by speed and cost, the four layers compare as follows:

| Layer | Speed | Setup and upkeep | Where a failure points |
| - | - | - | - |
| Static | Fastest (instant in the editor) | Lowest | The exact line |
| Unit | Fast | Low | The function and its input |
| Integration | Fast (slower than unit) | Medium (boundary mocks) | The component |
| E2E | Slow | High (deployed environment) | Somewhere along the journey |

The layers trade cost for confidence in opposite directions. A higher layer runs the system closer to the way users experience it, so one passing test covers more and means more; that same test is slower to run, needs more setup, and reports failures further from the cause.

A lower layer is fast, cheap, and precise: a failing type check names the exact line. What it cannot do is prove that the parts work together, because each check sees only a small part in isolation.

The trophy's shape encodes where the balance lands for frontend code. The integration layer holds the largest share of tests because a component test exercises rendering, state, and interaction together, while still running quickly in the pull request pipeline against a mocked backend.

> [!TIP]
>
> Each layer must work independently, and a degradation in the quality a layer owns must be detected by that layer's tests. A type error should not be caught first by a unit test, and an exception thrown by a function should not be caught only by an integration test. Type errors belong to the static layer and exceptions to the unit layer: each layer verifies the product code with a different approach, and together they support robust quality.

Each layer maps to a small, fixed set of tools:

| Layer | Mainly catches | Tools |
| - | - | - |
| Static | Type mismatches, undefined references, known defect patterns | TypeScript, ESLint |
| Unit | Wrong results from a function or module | Vitest |
| Integration (behavior) | A screen misbehaving on user interaction | Vitest + Testing Library |
| Integration (appearance) | Unintended visual changes | Storybook + visual regression testing |
| E2E | Broken wiring between frontend, backend, and database | Playwright |

Lower layers run in the pull request pipeline on every push; the E2E suite runs after deployment as the release check. See [CI/CD](/development/ci-cd.md) for the pipeline design and [E2E Testing](/frontend/e2e-testing.md) for why E2E runs post-deploy.

### Static tests

Static tests are the type check and the linter: verification that runs without executing the code. Once configured, they cover the entire codebase on every run at no extra cost per verification, which is why they form the base of the trophy.

The type check rules out invalid states instead of detecting them. A call that the type system rejects does not compile, so no runtime test needs to enumerate those values. Before writing a test for invalid input, check whether a type can make that input impossible.

```typescript TypeScript theme={null}
// ❌ A runtime test enumerates values the type system could reject
test("throws on an unknown status", () => {
  expect(() => statusLabel("shipped!")).toThrow();
});

// ✅ A union type turns the invalid call into a compile error,
// and the runtime test becomes unnecessary
type OrderStatus = "pending" | "paid" | "shipped";

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

// statusLabel("shipped!"); // compile error
```

The linter complements the type check with known defect patterns that types cannot express: a hook that reads a value missing from its dependency array, or markup that assistive technology cannot interpret. Both checks run in the editor and in the pull request pipeline, so this layer gives the fastest feedback of all.

The limit of this layer is behavior. A function can pass the type check and still return the wrong value; from here upward, the code has to run.

### Unit tests

Unit tests run a single function or module in isolation and verify its output for a given input. They suit pure logic with many input patterns, such as normalizing a date range or computing a total. The tool is Vitest.

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

// Verified through input and output alone: no rendering, no interaction
test("swaps the dates when the range is reversed", () => {
  expect(
    normalizeDateRange({ from: "2026-10-09", to: "2026-10-01" })
  ).toEqual({ from: "2026-10-01", to: "2026-10-09" });
});
```

The strength of this layer is speed and precision. A test runs one function, so a failure names the function and the input that went wrong. A custom hook's return value can also be verified here, but anything that involves rendering or interaction belongs to the integration layer.

This layer cannot verify composition. A function that is correct on its own does not guarantee that the screen behaves correctly once the function is wired into a component; that is the job of the integration layer.

### Integration tests

Integration tests combine components and hooks and verify them from a viewpoint close to the user's. The boundary with the backend is mocked, and the result of rendering and interaction is observed through the screen.

This layer holds two kinds of verification. Behavior tests render a component with Testing Library, simulate clicks and typing, and check how the screen changes; backend responses are replaced through a test provider or a mocking tool, so the test also covers how realistic data is displayed. Appearance tests render Storybook stories and detect unintended changes with visual regression testing.

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

// Render the screen and observe how it responds to user interaction
test("shows an error and disables search for an invalid range", async () => {
  const user = userEvent.setup();
  render(<OrderSearchForm />);

  await user.type(screen.getByLabelText("From"), "2026-10-09");
  await user.type(screen.getByLabelText("To"), "2026-10-01");

  expect(screen.getByRole("alert")).toHaveTextContent("must be on or after");
  expect(screen.getByRole("button", { name: "Search" })).toBeDisabled();
});
```

> [!NOTE]
>
> Unit and integration tests use the same test runner (Vitest), so in practice they are often written without distinction. This page separates them by whether rendering or interaction is involved.

This layer cannot verify the connection to the real backend. As long as the response is mocked, the test keeps passing even after the real API contract changes.

### E2E tests

E2E tests drive a real browser against a deployed environment and verify that a primary journey, from signing in to seeing results, completes through the whole system. The tool is Playwright.

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

// The signed-in state is reused via storageState, not repeated through the UI
test("a signed-in user searches orders and sees results", async ({ page }) => {
  await page.goto("/orders");
  await page.getByLabel("From").fill("2026-10-01");
  await page.getByLabel("To").fill("2026-10-09");
  await page.getByRole("button", { name: "Search" }).click();

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

Only this layer verifies the wiring between frontend, backend, and database. In exchange, it runs slowly and a failure can originate anywhere along the journey, so the suite stays small and limited to primary journeys.

E2E runs after deployment as the release check. For how to design the suite and the Playwright patterns, see [E2E Testing](/frontend/e2e-testing.md).

## Choosing a layer

When adding a verification, walk the layers from the bottom and place it in the first one that can catch the failure. Ask four questions in order:

* If a type or lint rule can guarantee it, place it in the static layer.
* If it can be verified as the input and output of a single function, place it in the unit layer.
* If it can be observed through rendering or interaction with the backend mocked, place it in the integration layer.
* If none of these apply and it can only be verified with the whole system running, place it in the E2E layer.

Applying these criteria to one screen, an order search form, spreads its verifications across all four layers:

| Verification | Layer | Reason |
| - | - | - |
| The search parameter names match the API contract | Static | The API client's types reject a typo at compile time |
| A date range is normalized correctly | Unit | Pure input and output with many edge cases |
| An invalid range shows an error and disables the search button | Integration | Needs rendering and interaction, but not a real backend |
| A signed-in user searches and sees results | E2E | Breaks only when frontend, backend, and database run together |

When two layers could catch the same failure, choose the lower one. The lower test runs on every push instead of after deployment, and its failure names the cause instead of the symptom.

This rule is also what keeps the E2E suite small: anything a component test can catch stays out of E2E, and E2E keeps only the journeys that verify the whole system. See the principles in [E2E Testing](/frontend/e2e-testing.md).

> [!NOTE]
>
> Pushing a verification down does not remove the need for the layers above it. A component test with a mocked backend cannot detect a broken contract with the real API; each layer keeps the failures only it can see.

## Related pages

The following pages cover the E2E layer and the shared principles of protective tests in depth.

> **[E2E Testing](/frontend/e2e-testing.md)**
>
> Protecting primary user journeys with a small E2E suite, and stable patterns with Playwright.

> **[Test Code](/development/testing.md)**
>
> The principles of protective tests: trustworthy passing and failing, and mocking only at boundaries.

> **[CI/CD](/development/ci-cd.md)**
>
> The pipeline where each layer runs, from commit-time checks to post-deploy E2E.


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