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.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.
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.
Each layer maps to a small, fixed set of tools:
Lower layers run in the pull request pipeline on every push; the E2E suite runs after deployment as the release check. See CI/CD for the pipeline design and E2E Testing 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
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.Vitest
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.Vitest + Testing Library
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.
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.Playwright
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.
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.
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
Protecting primary user journeys with a small E2E suite, and stable patterns with Playwright.
Test Code
The principles of protective tests: trustworthy passing and failing, and mocking only at boundaries.
CI/CD
The pipeline where each layer runs, from commit-time checks to post-deploy E2E.