What to Test in a React App
IntermediateTest behaviour a user can observe, not implementation. The test that breaks when you rename a state variable was never testing anything worth protecting.
Overview
The purpose of a test is to let you change code confidently. A test coupled to internals does the opposite: it fails on every refactor while catching none of the bugs users actually hit. Testing Library exists to push you towards the alternative — render the component, interact with it the way a person would, and assert on what appears. This concept covers setup and the judgement of what deserves a test; the mechanics of queries and events were introduced in the JavaScript track and are applied throughout the rest of this category.
Setup
Vitest with jsdom, Testing Library, and the matchers.
// vitest.config.js
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.js',
globals: true,
},
})
// src/test/setup.js
import '@testing-library/jest-dom/vitest' // toBeInTheDocument etc.
import { cleanup } from '@testing-library/react'
afterEach(cleanup) // automatic with globals: true
// A render helper, so every test gets the providers the app has
export function renderWithProviders(ui, { route = '/' } = {}) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }, // no retries in tests
})
return {
user: userEvent.setup(),
...render(
<MemoryRouter initialEntries={[route]}>
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
</MemoryRouter>,
),
}
}
// A fresh QueryClient per test, or cached data leaks between them.Behaviour, Not Implementation
The same component, tested two ways.
// Implementation-coupled — breaks on any refactor, catches nothing
expect(component.state.isOpen).toBe(true)
expect(container.querySelector('.modal-open')).toBeTruthy()
expect(useProblemsSpy).toHaveBeenCalled()
// Behaviour — survives a rewrite, fails only when the user's
// experience actually changes
it('shows the hint after the user asks for it', async () => {
const { user } = renderWithProviders(<ProblemPanel problem={problem} />)
expect(screen.queryByText(/use a hash map/i)).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: /show hint/i }))
expect(await screen.findByText(/use a hash map/i)).toBeInTheDocument()
})
// The test above would pass whether the component uses useState,
// useReducer, or a store — which is exactly the point.
// Rewrite the component's internals entirely: the test should still
// pass. If it fails, it was testing the wrong thing.Deciding What Earns a Test
Priorities, and what to leave alone.
// Worth testing, roughly in order:
// - conditional rendering: gates, permissions, empty vs error states
// - user flows: fill the form, submit, see the result
// - logic with branches: pricing, scoring, validation, date handling
// - anything that has broken before — a regression test per bug
// - custom hooks with real logic
// Not worth testing:
// - that a prop renders verbatim
// - CSS classes and styling
// - third-party library internals
// - trivial getters and pass-through wrappers
// A pure function is far cheaper to test than a component. Extract
// the logic and test it directly:
expect(calculateReadiness(attempts)).toEqual({ score: 26, provisional: false })
// Coverage is a map of what is untested, not a target. 100% coverage
// of trivial components while the scoring logic is untested is worse
// than 60% concentrated where the risk is.Key Points to Remember
- 1A test should survive an internal rewrite and fail only when observable behaviour changes
- 2Create fresh providers and a fresh QueryClient per test, and disable retries so failures surface immediately
- 3Prioritise conditional rendering, user flows, branchy logic and regression tests for past bugs
- 4Extract pure logic and test it directly — far cheaper than testing it through a component
- 5Coverage shows what is untested; concentrate it where the risk is rather than chasing a percentage
Interview Questions
Sign in to ask AriaWhat makes a React test brittle?
How do you decide which parts of a component are worth testing?
Why create a new QueryClient for each test?
Ask Aria about What to Test in a React App
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.