React Testing Library: Providers, MSW & Best Practices
Custom render with Providers
// src/test/test-utils.tsx — wrap render with all global providers
import { render, RenderOptions } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import { ThemeProvider } from './ThemeProvider'
function AllProviders({ children }: { children: React.ReactNode }) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }, // no retries in tests
})
return (
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<ThemeProvider theme="light">
{children}
</ThemeProvider>
</MemoryRouter>
</QueryClientProvider>
)
}
const customRender = (ui: React.ReactElement, options?: RenderOptions) =>
render(ui, { wrapper: AllProviders, ...options })
// Re-export everything from RTL but override render
export * from '@testing-library/react'
export { customRender as render }
// Usage — import from test-utils instead of @testing-library/react
import { render, screen } from '../test/test-utils'MSW — Mock Service Worker
// npm install --save-dev msw
// MSW intercepts network requests at the service worker / Node.js level
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
])
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ id: 2, ...body }, { status: 201 })
}),
http.get('/api/users/:id', ({ params }) => {
if (params.id === '999')
return new HttpResponse(null, { status: 404 })
return HttpResponse.json({ id: params.id, name: 'Bob' })
}),
]
// src/mocks/server.ts (Node.js — for tests)
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// src/test/setup.ts
import { server } from '../mocks/server'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
// Override handler per test
test('shows error on server failure', async () => {
server.use(http.get('/api/users', () => new HttpResponse(null, { status: 500 })))
render(<UserList />)
expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument()
})Best Practices
Query priority: ByRole > ByLabelText > ByText > ByTestId. If you need ByTestId, consider improving accessibility.
Avoid testing implementation details: don't test state, refs, or internal method calls. Test what the user sees and can interact with.
Use userEvent over fireEvent: userEvent simulates real browser behavior including all intermediate events.
Each test should be independent: don't share mutable state between tests. Use beforeEach to reset.
Use describe blocks to group related tests but avoid deeply nested describes — flat is easier to read.
Test accessibility: use ByRole queries and check that screen readers would work correctly.
Avoid act() warnings: they indicate uncaptured state updates. Use waitFor or findBy instead of manually wrapping in act.
Don't test every render: focus on behavior and interactions, not that a component renders X elements.
Snapshot tests: useful for catching unintended changes, but don't over-rely on them. Prefer explicit assertions.
// ❌ Testing implementation details
expect(wrapper.state().isLoading).toBe(false)
expect(component.instance().handleSubmit).toHaveBeenCalled()
// ✅ Testing user-visible behavior
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
expect(onSubmit).toHaveBeenCalledWith({ email: 'alice@example.com' })
// ❌ Querying by test ID when a better option exists
screen.getByTestId('submit-button')
// ✅ Query by role (also validates accessibility)
screen.getByRole('button', { name: /submit/i })
// ❌ Not cleaning up between tests (can cause cross-test pollution)
let component: RenderResult
beforeAll(() => { component = render(<MyComponent />) })
// ✅ Fresh render per test — RTL auto-cleans after each test
test('does something', () => {
render(<MyComponent />)
// ...
})Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free