
Most testing advice for frontend teams either tests too much at the wrong level or ignores the things users actually care about. Here's what works in practice.
Frontend testing is one of those subjects where there's no shortage of opinions, but the advice rarely maps cleanly to the reality of maintaining a production codebase. Teams either end up writing too many unit tests for things that don't matter and too few tests for the things that do, or they invest in an end-to-end suite that takes forty minutes to run and fails intermittently in CI. Neither situation is where you want to be.
The testing strategy that has actually worked across the teams and codebases I've been part of is organized around a simple question: what would have to break for a user to notice? The answer to that question determines what's worth testing and at what level of the stack.
Testing implementation details gives you a false sense of coverage. Tests that break when you rename a variable but pass when a feature is broken are worse than no tests.
The problem with unit testing React components
Unit testing individual components in isolation sounds rigorous, but in practice it often produces tests that test the wrong things. Testing that a component renders a div with a certain className. Testing that a prop gets passed to a child component. Testing that a function inside the component is called with specific arguments. These tests are coupled to the implementation, not the behavior. They break during refactors that didn't change anything users experience, and they pass when actual user-facing bugs are introduced.
The framing that changed how I think about this is from Kent C. Dodds: write tests that resemble how your software is used. A user doesn't see component internals. They see the screen, they interact with it, and they observe outcomes. Tests that simulate that — render a component tree, interact with it the way a user would, assert on what the user sees — are much more resilient to refactoring and much more likely to catch real bugs.
Integration tests: the most valuable layer
For React applications, the sweet spot is integration tests that render a meaningful slice of the application — a route, a feature, a page — with real component trees but mocked network requests. These tests exercise the actual user interactions, including state management, routing, and component composition, without the flakiness and slowness of a full browser environment.
With Vitest and React Testing Library, these tests are fast enough to run on every commit. They're isolated enough to be deterministic. And they're realistic enough to catch the kinds of bugs that matter: forms that don't submit, error states that don't render, navigation that goes to the wrong place.
- Unit tests: pure utility functions, complex business logic, custom hooks in isolation
- Integration tests (the bulk): full feature/page renders with mocked APIs
- E2E tests: critical user journeys only — login, checkout, key conversion flows
- Skip: testing component internals, prop forwarding, implementation details
What's worth unit testing
Unit tests do have a place. The right targets are things with complex logic that can be tested in complete isolation without rendering anything: utility functions, date formatters, validation logic, data transformation functions, custom hooks that manage non-trivial state machines. These tests run in milliseconds, are easy to write, and actually benefit from isolation — you can test every edge case without needing a browser environment.
The distinction is that these are functions, not components. A function that calculates a discount percentage, parses a URL, or validates a credit card number — that's worth unit testing exhaustively. A React component that conditionally renders some text based on a prop — test it by rendering and checking what the user sees, not by asserting on the conditional logic inside.
End-to-end tests: selective and stable
E2E tests with Playwright or Cypress are the most realistic and the most expensive. They're slow, they require a running application, and they can be flaky in ways that are hard to debug. The right answer isn't to avoid them — it's to use them selectively for the flows where that realism actually matters: authentication, payment, account creation, the journeys where a broken experience means lost revenue or lost users.
The failure mode to avoid is the comprehensive E2E suite that tests every feature end-to-end. That suite becomes a bottleneck, it's the first thing teams start skipping, and it gives a false confidence that integration tests could have provided more cheaply. E2E tests should make you confident in your critical paths, not replace the rest of your testing strategy.
Conclusion
The testing strategy that holds up over time is the one that tests behavior, not implementation. It's weighted toward integration tests, selective about unit tests, and disciplined about keeping E2E tests focused on the paths that truly can't be tested any other way.
More importantly, it's a strategy the team actually follows. Tests that are too slow to run, too brittle to trust, or too disconnected from real usage to be meaningful — those tests don't get written, don't get maintained, and eventually get deleted. The goal isn't coverage metrics. It's confidence that the things users do actually work.