React Testing Best Practices
Master advanced React testing including async assertions, Mock Service Worker (MSW), custom hooks, and Provider wrappers.
TL;DR
- Await asynchronous rendering transitions cleanly using modern
findByRolequeries. - Intercept network requests reliably using
Mock Service Worker. - Test headless stateful logic in isolation using
renderHook.
Asynchronous Testing
findBy Query ResolutionAwait appearance of asynchronously fetched data.
test('loads user name', async () => {
render(<UserProfile id="1" />);
const heading = await screen.findByRole('heading', {
name: /alex johnson/i,
});
expect(heading).toBeInTheDocument();
});waitFor Assertion GuardWait until expectation passes without error.
await waitFor(() => {
expect(screen.queryByText(/loading/i)).toBeNull();
});Avoid Act WarningsAwaiting findBy automatically flushes act transitions.
// Clean: findBy awaits effect completion
const card = await screen.findByText('Ready');Mocking APIs with MSW
Define MSW HandlersIntercept HTTP requests at network boundary.
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/user', () => {
return HttpResponse.json({ name: 'Alex' });
}),
];Server Lifecycle in TestsConfigure MSW listener before and after test suite.
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());Simulate Server Error ResponseOverride handler per test to verify error UI.
server.use(
http.get('/api/user', () => {
return new HttpResponse(null, { status: 500 });
})
);Testing Custom Hooks with renderHook
renderHook APIMount custom hook in headless test environment.
import { renderHook } from '@testing-library/react';
import { act } from '@testing-library/react';
test('increments count', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => result.current.inc());
expect(result.current.count).toBe(1);
});Hook with Initial PropsPass dynamic props into hook during test run.
const { result, rerender } = renderHook(
({ step }) => useCounter(step),
{ initialProps: { step: 1 } }
);
rerender({ step: 5 });Async Hook TestingAwait async state transitions inside custom hook.
const { result } = renderHook(() => useAsyncData());
await waitFor(() => {
expect(result.current.isDone).toBe(true);
});Testing Context and Providers
Custom Render WrapperBuild reusable render function wrapping all providers.
function AllProviders({ children }: Props) {
return (
<QueryClientProvider client={testQueryClient}>
<ThemeProvider>{children}</ThemeProvider>
</QueryClientProvider>
);
}
const customRender = (ui: any) =>
render(ui, { wrapper: AllProviders });Consumer Test IsolationTest consumer component with mocked context value.
render(
<AuthContext.Provider value={{ user: 'TestUser' }}>
<UserHeader />
</AuthContext.Provider>
);Provider Invariant Error TestAssert that hook throws error outside Provider.
expect(() => renderHook(() => useAuth())).toThrow(
'useAuth must be in AuthProvider'
);Tips
- Always prefer
screen.findByRoleoverwaitForbecause findBy queries have built-in retry intervals and automatic timeout error reporting. - Use
Mock Service Worker(MSW) to intercept HTTP requests at the network layer rather than mockingfetchmodules directly.
Warnings
- Avoid wrapping test assertions inside arbitrary
sleepor timeout delays; always synchronize assertions usingwaitFororfindBy. - Do not test third-party library internals; verify only the contracts and behaviors your own components consume and
expose.
In Practice
Test an asynchronous data fetching component, verifying loading placeholder dismissal and heading appearance using findByRole.
- Render component initiating asynchronous data fetch on mount.
- Verify that temporary loading skeleton appears initially.
- Await final heading resolution using screen.findByRole.
- Confirm that loading skeleton unmounts cleanly from DOM.
import { render } from '@testing-library/react';
import { screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';
test('renders user profile cleanly', async () => {
render(<UserProfile id="user-1" />);
const loader = screen.getByText(/loading/i);
expect(loader).toBeInTheDocument();
const heading = await screen.findByRole('heading', {
name: /alex johnson/i,
});
expect(heading).toBeInTheDocument();
expect(screen.queryByText(/loading/i)).toBeNull();
});FAQ
Create a custom render wrapper function that automatically wraps the tested component in necessary Providers (Theme, Auth, QueryClient) before executing queries.
MSW intercepts requests at the network level using Service Workers, keeping component code completely untouched and testing real fetch behavior.
Use the renderHook utility from React Testing Library and wrap any imperative hook triggers inside act() to ensure state updates flush synchronously.