Back to React tutorials
Intermediate14 min read

Testing

Test React components and hooks with React Testing Library, focusing on user-visible behavior.

Testing Philosophy

React Testing Library encourages testing how users interact with UI — queries by role, label, and text — not implementation details like state or internal methods. Tests survive refactors that preserve behavior.

Avoid testing component state directly or shallow rendering entire subtrees. Assert visible outcomes: text appears, button disables, form submits.

  • Query by role, label, text
  • Assert user-visible outcomes
  • Avoid testing implementation details
render(<LoginForm />);
await userEvent.type(screen.getByLabelText(/email/i), 'a@b.com');
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));

Rendering and Queries

render mounts components in jsdom. Priority queries: getByRole, getByLabelText, getByPlaceholderText, getByText, getByTestId (last resort). Use findBy* for async appearance, queryBy* when asserting absence.

within() scopes queries to containers. screen debug() prints DOM for troubleshooting failing tests.

  • Prefer getByRole
  • findBy for async elements
  • queryBy for absence assertions

User Events and Async

userEvent simulates realistic interactions — click, type, tab, select. Prefer over fireEvent for integration fidelity. waitFor wraps assertions that depend on async updates.

Mock network with MSW (Mock Service Worker) at fetch level rather than mocking fetch implementation — tests exercise real fetch code paths with controlled responses.

  • userEvent over fireEvent
  • waitFor async assertions
  • MSW for network mocking

Testing Hooks and Providers

renderHook tests custom hooks in isolation. Wrap with providers matching production setup. Pass wrapper option to render and renderHook.

Reset modules and cleanup after each test — RTL cleanup unmounts and clears DOM. Isolate tests — no shared mutable state between cases.

  • renderHook for custom hooks
  • Provider wrappers in tests
  • cleanup after each test
const { result } = renderHook(() => useCounter(), {
  wrapper: ({ children }) => <Provider>{children}</Provider>,
});

Integration and E2E

Integration tests render feature slices with real routing and data providers. E2E tests (Playwright, Cypress) cover critical user journeys across real browsers.

Pyramid balance: many unit/integration tests for speed, fewer E2E for confidence on checkout, auth, and payment flows. Run E2E in CI on pull requests per team policy.

  • Integration tests for feature slices
  • E2E for critical journeys
  • CI runs on pull requests

Get In Touch


Ready to discuss your next project? Drop me a message.