React Testing Library
03 / 03

Providers, MSW & Best Practices

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