Skip to content

Testing

What to test, which tools to reach for, and how to run the whole suite in CI without it becoming the thing everyone ignores. Part of the Frontend Roadmap. See also Accessibility for the manual half of a11y work that no tool can automate, Performance for the metrics the budgets below enforce, TypeScript for the static layer underneath every test here, and Tooling for the build and CI setup the suite runs inside.

Tests exist so you can change code without fear. That is the entire return, and every testing decision answers to it: a test that fails only when a user would have noticed is doing its job, and a test that fails when you rename a function is charging you rent.

The shape most frontend teams land on is the testing trophy rather than the classic pyramid. Static checks sit at the base and cost nothing per test: TypeScript and a linter catch a whole class of bug before you write an assertion. Most of the deliberate effort goes into component tests: render the real markup, click it the way a person would, assert on what they would see. That layer buys the most confidence per line of test code, because it exercises the wiring between components, which is where frontend bugs actually live. Above it, a thin set of end-to-end tests covers the few flows the business genuinely depends on — sign-up, checkout, the one report everybody exports. Unit tests stay for logic that is tricky on its own: date maths, parsers, pricing rules.

The failure mode worth naming is testing implementation details — asserting on state, props, or CSS class names instead of rendered output. Those tests break on refactors that changed nothing a user could see, and sail past bugs a user would hit on the first click. Query what a screen reader would announce, interact the way a person would, assert on what they would see.

A default stack. For anything new: Vitest for unit and component tests, because it shares Vite’s config and transform pipeline so there is no second build setup to keep in sync, and it is fast enough to leave running in watch mode. Testing Library for driving the DOM, because it makes the accessible query the easiest one to write, so tests and accessibility improve together. Playwright for end-to-end, because it parallelises by default, runs real Chromium, Firefox, and WebKit, and its trace viewer turns a red CI run into something you can actually debug. Jest and Cypress are the alternatives, and you will meet both in existing codebases — neither is a mistake, and neither is what you would pick starting fresh in 2026.

Learn

The canonical essays, in the order they build on each other. They are short, they are the source of the vocabulary every testing tool now uses, and reading them first will save you from writing a suite you later delete.

Reference

Unit and component testing

Vitest is the default for new projects: it reads your existing Vite config, so aliases, plugins, and environment variables work in tests without a parallel build setup, and its watch mode is fast enough to keep open. Jest still runs an enormous share of existing codebases and is worth knowing for that reason alone — the APIs overlap almost completely, so what you learn transfers either direction.

  • Vitest — The default test runner for anything built on Vite. Jest-compatible API, native ESM and TypeScript, instant watch mode.
  • Why Vitest — The case for it over Jest, including the config-duplication problem it exists to solve.
  • Vitest Browser Mode — Running component tests in a real browser instead of jsdom, for when the simulated DOM stops being close enough.
  • Jest — The incumbent runner, and the one most existing projects use. Also the source of the describe/it/expect vocabulary everything else copied.
  • Node.js Test Runner — The runner built into Node, with no dependency to install. Enough for library and utility code that never touches the DOM.

Testing UI

One idea powers this whole family of libraries: the more your tests resemble the way your software is used, the more confidence they can give you. In practice that means finding elements by their accessible role and name rather than by test IDs or CSS selectors, which is also why a suite written this way keeps catching accessibility regressions as a side effect.

  • Testing Library — The hub for the whole family, with adapters for React, Vue, Svelte, Angular, and plain DOM.
  • Guiding Principles — The short page that explains every API decision in the library. Read it before the API docs.
  • React Testing Library — Rendering React components and asserting on output rather than internals. The standard way to test React in 2026.
  • About Queries — The query priority list: getByRole first, getByTestId last, and the reasoning for that order.
  • user-event — Simulates real interaction — focus, key sequences, pointer events — instead of firing a single synthetic event.
  • jest-dom — DOM matchers such as toBeVisible and toHaveAccessibleName, which turn assertion failures into readable sentences.
  • Testing Playground — Paste markup, click an element, get the query you should be using. The fastest way to learn the priority order.

End-to-end

End-to-end tests are the only ones that prove the whole system works, and the most expensive to keep green — so cover the few flows that would cost real money if they broke, and stop. Playwright is the default: parallel by default, one API across Chromium, Firefox, and WebKit, and auto-waiting locators that remove the sleep-and-retry code that makes suites flaky. Cypress has the friendlier debugging experience and a large body of existing tests, but runs in-browser, which constrains multi-tab and cross-origin work.

  • Playwright — Cross-browser end-to-end testing with auto-waiting, parallel execution, and a code generator to bootstrap the first test.
  • Playwright Best Practices — What to test, what to isolate, and which habits produce flaky suites. Short and worth re-reading later.
  • Locators — Playwright’s selector model, built around accessible roles and labels rather than CSS paths.
  • Fixtures — Setup and teardown as injectable values, so authentication and seeded data are declared per test rather than in shared hooks.
  • Parameterised Tests — Running one test body across a table of test data, including reading cases from a CSV.
  • Trace Viewer — A recording of a failed run — DOM snapshots, network, console — which is what makes CI failures debuggable without reproducing them.
  • Why Cypress? — The alternative, and its architecture: tests run inside the browser, with time-travel debugging.
  • Cypress Best Practices — The official anti-pattern list. Useful even on Playwright, since most entries are about test design, not Cypress.

Mocking and test data

Mock at the network boundary, not inside your code. Stubbing fetch couples the test to how the request is made, so swapping to a different client breaks tests without breaking behaviour; intercepting at the network layer leaves your code untouched and lets the same handlers serve unit tests, end-to-end tests, and local development. And prefer not to mock at all where the real thing is fast and deterministic — every mock is a copy of an interface that can drift from it.

  • Mock Service Worker — Intercepts requests at the network level, so application code makes real calls and never knows it is being tested.
  • MSW Philosophy — The argument for intercepting instead of stubbing, and what mocking the client costs you. This is the “why” page.
  • Mock APIs — Intercepting and stubbing network traffic in end-to-end tests, plus recording real responses to replay later.
  • Mocking — Vitest on mocking modules, timers, and globals, and which of those you should reach for last.
  • Faker — Generates realistic names, addresses, and dates, so fixtures stop being "test1" and start surfacing layout bugs.
  • Mocks Aren’t Stubs — The vocabulary — stub, mock, fake, spy — and the classicist argument for using far fewer of them. [advanced]

Type-level testing

Types catch a different class of error than tests do, and neither substitutes for the other: a fully typed function can still compute the wrong answer, and 100% type coverage says nothing about behaviour. What type-level tests are genuinely for is library work — asserting that a generic signature still infers what you promised after you refactor it. See TypeScript for the type system itself.

  • Testing Types — Vitest’s expectTypeOf and assertType, running type assertions alongside the rest of the suite.
  • expect-type — Compile-time assertions about inferred types, and the library Vitest’s type testing is built on.
  • type-coverage — Reports what percentage of your code is actually typed rather than silently any. A different number from test coverage, and a useful one.

Tools

Visual and regression testing

Screenshot testing catches the things assertions cannot describe — a broken grid, a z-index regression, a font that failed to load. Be honest about the cost before adopting it: snapshots go stale on every intentional design change, and they fail across operating systems, browser versions, and font rendering differences, so a suite without a stable container and a review workflow becomes a queue of diffs nobody approves. Start with the two or three screens that matter most.

  • Visual Comparisons — Playwright’s built-in screenshot assertions, with per-platform baselines and a configurable diff threshold. Free, and enough to start with.
  • Storybook Test Runner — Turns every story into an executable test, so the component catalogue doubles as the smoke suite.
  • Chromatic — Hosted visual review for Storybook: renders on their infrastructure and turns diffs into a per-PR approval step. [freemium]
  • Argos — Open-source visual review that ingests screenshots from Playwright or Cypress, so the tests stay in your repo. [freemium]

Accessibility testing

Put a bound on what this buys you: automated checks find some of the real accessibility problems, never most of them. The figure usually quoted is 20–30% of issues; Deque’s own study of axe-based testing argues for around 57%, and even that leaves nearly half undetected. Contrast ratios and missing labels are detectable; whether the focus order makes sense, whether an error message actually explains the error, and whether the page is usable by keyboard alone are not. Automated a11y tests are a floor, not a pass — manual keyboard and screen reader testing is still required. See Accessibility for how to do that half.

  • axe-core — The rules engine behind nearly every accessibility checker, browser extensions included. The tools below all wrap it.
  • jest-axe — The axe engine as a single matcher in Jest or Vitest, so a component test can also assert the component has no obvious violations.
  • Accessibility Testing — Running axe over real pages in end-to-end tests, including how to scope scans and handle known issues.
  • Accessibility Tests — Checking every story as you build it, which is the cheapest place to catch a violation.
  • Automated Accessibility Testing Coverage — The measured numbers behind the caveat above, from the people who wrote the engine.

Performance testing

Performance regressions arrive one dependency at a time, which is exactly what CI is good at catching. Two checks cover most of it: a Lighthouse run against a deployed preview, and a hard byte budget on the bundle that fails the build when someone imports a date library to format one timestamp. See Performance for what the numbers mean.

  • Lighthouse CI — Runs Lighthouse on every commit and asserts against thresholds, so a score drop blocks the PR instead of being noticed a quarter later.
  • Size Limit — Fails CI when the bundle exceeds a byte budget you set, and reports the download and execution time that budget implies.
  • Performance Budgets 101 — How to pick numbers that are defensible rather than arbitrary, and where to enforce them.

Testing in CI

A suite only pays off when it runs on every pull request, and it only keeps paying off if it stays trustworthy — one test that fails at random teaches the whole team to re-run the job instead of reading it, and from then on the suite catches nothing. Quarantine flaky tests immediately and fix them properly. On coverage: use it to find untested areas, never as a target. 100% coverage is achievable with tests that assert nothing, and chasing it produces exactly those tests.

  • Building and Testing Node.js — The GitHub Actions workflow to copy: install, cache, matrix across Node versions, run the suite.
  • Playwright on CI — Running browsers in CI, plus uploading the HTML report and traces so failures are diagnosable after the fact.
  • Sharding — Splitting one suite across parallel machines and merging the reports, which is how a long end-to-end run stays under ten minutes.
  • Coverage — Vitest coverage via V8 or Istanbul, including how to report it per PR and where a threshold is genuinely warranted.

Practice

  • JavaScript & Node.js Testing Best Practices — Around fifty bullet-pointed practices with code for each. The best single review checklist for your own tests.
  • Cypress Real World App — A full payment application with the test suite a real product would have. Read the tests before you write your own.
  • Testing Library Examples — Runnable sandboxes for the common cases, so you can break a passing test and see what it says.
  • Playwright TodoMVC Demo — The app Playwright’s own example suite drives. A safe target for a first end-to-end test against something you did not build.
  • Kata-Log — Small, repeatable exercises for practising test-driven development on problems that fit in one sitting.

Deep dives

  • Test Coverage — Why coverage is a useful diagnostic and a terrible target, in about four hundred words.
  • Eradicating Non-Determinism in Tests — The systematic causes of flakiness — shared state, async, time, resource leaks — and the fix for each. [advanced]
  • Flaky Tests at Google — What flakiness costs at scale, with the numbers, and how they keep a suite of that size credible.
  • UnitTest — Why nobody agrees on what a unit test is, and the solitary-versus-sociable distinction that makes the argument tractable.