Cheat SheetsReactTesting

Testing — Cheat Sheet

React · 4 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Testing
React4 topicsQuick revision reference
1

What to Test in a React App

Test behaviour a user can observe, not implementation. The test that breaks when you rename a state variable was never testing anything worth protecting.

  • A test should survive an internal rewrite and fail only when observable behaviour changes
  • Create fresh providers and a fresh QueryClient per test, and disable retries so failures surface immediately
  • Prioritise conditional rendering, user flows, branchy logic and regression tests for past bugs
  • Extract pure logic and test it directly — far cheaper than testing it through a component
  • Coverage shows what is untested; concentrate it where the risk is rather than chasing a percentage
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.
2

Testing Custom Hooks

renderHook runs a hook without a component. Use it for hooks with real logic, and test the rest through the components that use them.

  • renderHook runs a hook without a component and exposes its latest return value at result.current
  • Destructuring result.current captures a stale snapshot — always read it fresh after an update
  • Wrap state-changing calls in act so React flushes updates and effects before your assertions
  • rerender with new props tests hooks that react to changing input; a wrapper supplies required context
  • Test hooks with real logic directly, thin wrappers through their component, and pure logic as plain functions
result.current is live; wrap updates in act
import { renderHook, act } from '@testing-library/react'

it('increments', () => {
  const { result } = renderHook(() => useCounter(0))

  expect(result.current.count).toBe(0)

  act(() => { result.current.increment() })     // state update -> act

  expect(result.current.count).toBe(1)
})

// result.current is always the LATEST return value. Destructuring
// it early captures a stale snapshot:
const { count } = result.current      // frozen at that moment
act(() => result.current.increment())
expect(count).toBe(1)                 // fails — count is still 0

// act tells React to flush the update and its effects before the
// next assertion. Without it you get the "not wrapped in act"
// warning and assertions that run against the previous render.

// userEvent and the async findBy queries call act internally, which
// is why component tests rarely need it explicitly.
3

Testing Async Behaviour and the Network

Mock at the network boundary with MSW so your real fetching code runs. Then test the states everyone forgets: error, empty, and slow.

  • Intercepting HTTP with MSW keeps your real fetching, error handling and parsing code under test
  • Set onUnhandledRequest to error so an unmocked call fails loudly instead of timing out
  • Reset handlers after each test so per-test overrides do not leak into the next
  • Test error, retry, empty and in-flight states — the paths nobody exercises manually
  • Use findBy and waitFor for anything asynchronous; getBy throws immediately and arbitrary sleeps cause flakiness
Intercept HTTP, not your own modules
// src/test/server.js
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'

export const server = setupServer(
  http.get('/api/problems', () => HttpResponse.json([
    { id: 1, slug: 'two-sum', title: 'Two Sum', difficulty: 'easy' },
  ])),
  http.post('/api/submissions', () => HttpResponse.json({ id: 9 }, { status: 201 })),
)

// setup.js
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())        // per-test overrides do not leak
afterAll(() => server.close())

// onUnhandledRequest: 'error' is important — an unmocked request
// fails loudly instead of hanging until the test times out.

// Override for one test
server.use(
  http.get('/api/problems', () => new HttpResponse(null, { status: 500 })),
)
4

Integration Tests — A Whole Feature

Rendering a real route with real providers and walking through a flow catches the wiring bugs that unit tests structurally cannot, at a fraction of the cost of an E2E suite.

  • Integration tests render a real route with real providers and catch bugs that live between units
  • One flow test can cover validation, submission, cache invalidation and navigation at once
  • MemoryRouter with an initial route makes guards, redirects and 404s testable without a browser
  • Build fixtures per test with factories and query by role, so tests do not depend on order or exact copy
  • Integration tests are the widest useful layer — faster than E2E and far more revealing than unit tests alone
One flow, four classes of bug
it('creates a problem and shows it in the list', async () => {
  const { user } = renderWithProviders(<App />, { route: '/admin/problems/new' })

  await user.type(screen.getByLabelText(/title/i), 'Two Sum')
  await user.selectOptions(screen.getByLabelText(/difficulty/i), 'easy')
  await user.click(screen.getByRole('button', { name: /publish/i }))

  // navigated
  expect(await screen.findByRole('heading', { name: 'Two Sum' })).toBeInTheDocument()

  // and the list was invalidated, not left stale
  await user.click(screen.getByRole('link', { name: /all problems/i }))
  expect(await screen.findByText('Two Sum')).toBeInTheDocument()
})

// This single test would have caught: a broken submit handler, a
// validation rule blocking valid input, a missing invalidation, and
// a wrong redirect. Four unit tests would have caught none of them,
// because each of those bugs lives BETWEEN the units.
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/react