Part 5.1 — React Testing Philosophy: What to Test (and What Not To)
Frontend testing is where many teams either over-test or under-test.
Both are expensive.
Over-testing leads to brittle suites that break on refactors.
Under-testing leads to fear, regressions, and fragile releases.
This post gives you a clear mental model for testing React applications so your tests deliver confidence—not friction.
1. The Goal of Frontend Tests
Frontend tests exist to answer one question:
“Would a user notice if this breaks?”
If the answer is no, you probably shouldn’t test it.
If the answer is yes, it deserves a test.
This mindset keeps your test suite lean and valuable.
2. The Testing Pyramid (Revisited for React)
For React apps, the pyramid looks like:
Many component tests (Jest + Testing Library)
Some integration tests (components + API mocks)
Few end-to-end tests (Playwright)
Notably missing:
snapshot-heavy tests
implementation detail tests
React’s strength is composition—tests should mirror that.
3. Test Behavior, Not Implementation
Bad test:
expect(useState).toHaveBeenCalled();Good test:
expect(screen.getByText('Loading…')).toBeInTheDocument();You don’t care how state is managed.
You care what the user sees.
Implementation details change. Behavior should not.
4. What You Should Test in React
You should test things that users experience directly.
Examples worth testing:
rendering of critical UI
loading and error states
form validation feedback
button enabled/disabled states
navigation between pages
conditional rendering
accessibility roles and labels
If breaking it would block a user task, test it.
5. What You Should Not Test
Avoid testing:
internal state variables
private helper functions
exact DOM structure
CSS class names
implementation details of hooks
third-party library behavior
Example to avoid:
expect(component.state.isOpen).toBe(true);This makes refactors painful and tests fragile.
6. Prefer User-Centric Queries
React Testing Library encourages this:
screen.getByRole('button', { name: /submit/i });Instead of:
container.querySelector('.btn-primary');User-centric queries:
make tests more accessible
mirror how assistive tech interacts
survive refactors
Accessibility-friendly code is test-friendly code.
7. Test the Edges, Not the Happy Path Only
Happy paths matter—but bugs live at the edges.
Examples:
submitting empty forms
slow network responses
API errors
disabled buttons
permission-restricted UI
Testing these cases gives disproportionate confidence.
8. Mock Strategically, Not Blindly
In frontend tests:
mock network calls
do not mock React itself
avoid mocking too deep
Mocking fetch is good.
Mocking component internals is not.
Your tests should resemble real usage as closely as possible without hitting the real backend.
9. Snapshot Tests: Use Sparingly
Snapshots are tempting—and dangerous.
Use snapshots only for:
small, stable UI pieces
icons
pure presentational components
Avoid snapshots for:
complex components
dynamic data
conditional rendering
Snapshots rot silently.
10. Coverage Is a Signal, Not a Goal
High coverage does not mean high confidence.
A few well-chosen tests often beat:
dozens of shallow assertions
inflated coverage numbers
Use coverage to find blind spots—not as a KPI.
11. When to Write an E2E Test Instead
Write a Playwright test when:
multiple components interact
navigation is involved
auth or permissions matter
regressions have occurred before
If a bug escaped before, lock it down with E2E.
12. A Practical Rule of Thumb
For every feature:
one component test for core behavior
one integration test if API interaction exists
one E2E test if the flow spans pages
Not a hard rule—but a good default.
13. Summary
You now have a clear philosophy:
test what users experience
avoid implementation details
favor behavior over structure
use component tests for most cases
use E2E tests for critical flows
treat coverage as guidance, not a target
In Part 5.2, you’ll turn this philosophy into reality by setting up Jest, React Testing Library, and a clean frontend testing environment.
Related
Part 6.6 — Async State, Side Effects & Server State Boundaries
Most React state bugs come from mixing UI state with server state. This post teaches you how to draw a hard line between them—and why that line matters.
Part 6.5 — Introducing Redux Toolkit the Right Way
Redux isn’t the default anymore—but when your app needs it, Redux Toolkit is the cleanest, safest way to introduce global state at scale.
Part 6.4 — Combining Context + useReducer for App-Level State
Context provides access. Reducers provide structure. Together, they form a powerful, lightweight architecture for managing app-level state in React.
Comments