Home/Learn/React/What to Test in a React App

What to Test in a React App

Intermediate
Testing

Test 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, jsdom, and a providers helper
// 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.

The refactor test
// 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.

Gates, flows, branchy logic, past bugs
// 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 Aria
1

What makes a React test brittle?

Medium
2

How do you decide which parts of a component are worth testing?

Medium
3

Why create a new QueryClient for each test?

Hard

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.

Loading discussion…