Testing React Applications
Learn unit, component, integration, accessibility, mocking, asynchronous tests, user-centric assertions, and test architecture.
What to Test
Tests should provide confidence at the right level. Pure utility functions are cheap to unit test. Components and user workflows benefit from integration-style tests. A smaller number of end-to-end tests validate critical journeys in a real browser.
Avoid testing implementation details such as private state variable names or internal hook calls. Prefer observable behavior: what the user sees, clicks, types, and receives.
The most valuable tests protect business behavior that would be expensive to break.
- Test behavior rather than implementation details.
- Use the lowest level that gives meaningful confidence.
- Protect critical user journeys with end-to-end coverage.
Component Testing
A component test should render the component in an environment close enough to production to exercise its behavior. Query elements by accessible role, label, or visible text when possible.
Avoid selecting elements solely by generated class names or implementation-specific DOM structure. Tests that depend on internals break during harmless refactors.
When a component needs data, provide a realistic boundary such as a mocked API layer rather than mocking every internal function.
render(<LoginForm onSubmit={submit} />);
await user.type(screen.getByLabelText(/email/i), "user@example.com");
await user.click(screen.getByRole("button", { name: /sign in/i }));
expect(submit).toHaveBeenCalled();- Query through accessible semantics.
- Test user-visible behavior.
- Avoid asserting implementation details.
Testing Async UI
Async components need tests that wait for the UI to reach a meaningful state. Avoid arbitrary sleep calls because they create slow and flaky tests.
Assert loading, success, empty, and error states. If a request is delayed, the test should observe the transition rather than depending on a specific number of milliseconds.
Mock at the network boundary when practical so the component exercises its real data-handling logic.
- Wait for observable UI conditions.
- Avoid arbitrary timeouts.
- Test both successful and failed asynchronous paths.
Mocking Without Losing Confidence
Mocks are useful for unstable external boundaries, but excessive mocking can produce tests that pass while the real integration is broken.
Mock the network, clock, browser API, or external service when necessary. Avoid mocking React itself or replacing every child component unless the child is truly an external boundary.
A good test suite balances isolation with realism.
- Mock boundaries, not internal implementation unnecessarily.
- Prefer realistic network-level mocks.
- Keep mocks simple enough to understand.
End-to-End Testing
End-to-end tests run the application in a browser-like environment and validate complete workflows. They are valuable for login, checkout, critical forms, routing, permissions, and other journeys where multiple systems must work together.
Because E2E tests are slower and more environment-sensitive, keep them focused. Use deterministic test data and isolate external dependencies where necessary.
A failing E2E test should make it easy to determine whether the problem is UI, API, authentication, data, or infrastructure.
- Use E2E for critical cross-system journeys.
- Keep test data deterministic.
- Design failures to be diagnosable.