- React
- Unit Testing, Async Mocking, and Debugging Workflows
- Testing Async Components: API Mocking & Mock Service Worker (MSW)
Testing Async Components: API Mocking & Mock Service Worker (MSW)
Testing async components requires intercepting network requests at the network layer using Mock Service Worker (MSW) rather than mocking fetch or axios globals. Use findBy queries to await rendered asynchronous data and assert loading, success, and error boundary states.
Key Learning Objectives
Mock API endpoints at the network layer using Mock Service Worker (MSW) for realistic tests.
Explain why mocking global fetch or axios creates fragile, coupled tests compared to MSW.
Use asynchronous `findByRole` queries to test loading spinners, resolved data, and error alerts.
Test server error scenarios (500 Internal Server Error, network drop) and user retry flows.
The Interview Problem & Context
Demonstrate how to test an async data-fetching component with loading state, resolved data state, and network failure state using Mock Service Worker (MSW).
1import React, { useState, useEffect } from 'react';2import { render, screen } from '@testing-library/react';3import userEvent from '@testing-library/user-event';4import { rest } from 'msw';5import { setupServer } from 'msw/node';67// Component under test8export function UserDirectory() {9 const [users, setUsers] = useState<string[]>([]);10 const [loading, setLoading] = useState(true);11 const [error, setError] = useState('');1213 const fetchUsers = async () => {14 setLoading(true);15 setError('');16 try {17 const res = await fetch('/api/users');18 if (!res.ok) throw new Error('Failed to load users');19 const data = await res.json();20 setUsers(data.map((u: any) => u.name));21 } catch (err: any) {22 setError(err.message);23 } finally {24 setLoading(false);25 }26 };2728 useEffect(() => {29 fetchUsers();30 }, []);3132 if (loading) return <div>Loading users...</div>;33 if (error) return <div role="alert">{error} <button onClick={fetchUsers}>Retry</button></div>;3435 return (36 <ul aria-label="Users List">37 {users.map((name, i) => <li key={i}>{name}</li>)}38 </ul>39 );40}4142// MSW Server Setup43const server = setupServer(44 rest.get('/api/users', (req, res, ctx) => {45 return res(ctx.json([{ name: 'Alice' }, { name: 'Bob' }]));46 })47);4849beforeAll(() => server.listen());50afterEach(() => server.resetHandlers());51afterAll(() => server.close());5253// Test 1: Successful Async Fetch54test('renders users list upon successful API response', async () => {55 render(<UserDirectory />);5657 // Assert loading indicator is present initially58 expect(screen.getByText(/loading users.../i)).toBeInTheDocument();5960 // findByRole automatically awaits async appearance!61 const list = await screen.findByRole('list', { name: /users list/i });62 expect(list).toBeInTheDocument();63 expect(screen.getByText('Alice')).toBeInTheDocument();64 expect(screen.getByText('Bob')).toBeInTheDocument();65});6667// Test 2: Network Error and Retry68test('displays error message and allows user to retry', async () => {69 server.use(70 rest.get('/api/users', (req, res, ctx) => res(ctx.status(500)))71 );7273 render(<UserDirectory />);7475 const errorAlert = await screen.findByRole('alert');76 expect(errorAlert).toHaveTextContent(/failed to load users/i);77});
Deep Architectural Explanation
Why Mock Service Worker (MSW) is the Gold Standard
The Problem with Mocking fetch or axios:
- If you write jest.spyOn(global, 'fetch').mockResolvedValue(...), your tests are tightly coupled to the exact client library used.
- If you switch from fetch to axios or TanStack Query, all your tests break, even though the application still makes identical HTTP GET requests!
- It does not intercept headers, status codes, or cookie serialization accurately.
How MSW Works:
- Network-Level Interception: MSW intercepts requests at the network layer (using Node's http/https interception in Jest, or Service Workers in browser).
- Library-Agnostic: Whether your component uses fetch, Axios, TanStack Query, or XMLHttpRequest, MSW intercepts it transparently.
- Declarative Handlers: You define request handlers matching HTTP method and route, returning mocked JSON and status codes.
- Per-Test Overrides: Using server.use(...), individual tests can simulate 404s, 500s, or delayed responses without polluting other tests.
Common Interview Traps & Pitfalls
Forgetting to await findBy queries when waiting for async data to render, resulting in immediate assertion failures.
Not calling server.resetHandlers() in afterEach, causing error handler overrides from one test to leak into subsequent tests.
Wrapping async tests in artificial sleep/timer delays instead of using findBy or waitFor.
Senior Interviewer Follow-Up Probes
How does MSW work in browser environments during local development without a backend?
How do you test race conditions where an earlier slow request resolves AFTER a later fast request?
How does TanStack Query provide a dedicated QueryClientProvider wrapper for unit tests?
Frequently Asked Interview Questions
Why is MSW preferred over jest.fn() mocking of fetch?
MSW intercepts actual network requests regardless of whether your code uses fetch, Axios, or TanStack Query. It avoids coupling test assertions to specific internal client implementation details.
How do findBy queries work under the hood?
findBy queries return a Promise that repeatedly executes a getBy query inside a MutationObserver until the element is found in the DOM or the timeout (default 1000ms) is reached.
How do you simulate a 500 server error with MSW?
Call server.use(rest.get("/api/endpoint", (req, res, ctx) => res(ctx.status(500)))) inside the specific test block to override default successful handlers.
What does server.resetHandlers() do?
It clears any runtime handler overrides introduced during individual tests, restoring the MSW server to its default configuration defined in setupServer.
How do you test components using TanStack Query?
Wrap the rendered component in a fresh QueryClientProvider with retry: false configured on QueryClient to prevent test timeouts on error retries.
