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