React Curriculum

Testing Async Components: API Mocking & Mock Service Worker (MSW)

Medium100% Free~30 mins#react#testing#msw#async#mocking#api-testing
Interview Answer (30-Second Summary)

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';
6
7// Component under test
8export function UserDirectory() {
9 const [users, setUsers] = useState<string[]>([]);
10 const [loading, setLoading] = useState(true);
11 const [error, setError] = useState('');
12
13 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 };
27
28 useEffect(() => {
29 fetchUsers();
30 }, []);
31
32 if (loading) return <div>Loading users...</div>;
33 if (error) return <div role="alert">{error} <button onClick={fetchUsers}>Retry</button></div>;
34
35 return (
36 <ul aria-label="Users List">
37 {users.map((name, i) => <li key={i}>{name}</li>)}
38 </ul>
39 );
40}
41
42// MSW Server Setup
43const server = setupServer(
44 rest.get('/api/users', (req, res, ctx) => {
45 return res(ctx.json([{ name: 'Alice' }, { name: 'Bob' }]));
46 })
47);
48
49beforeAll(() => server.listen());
50afterEach(() => server.resetHandlers());
51afterAll(() => server.close());
52
53// Test 1: Successful Async Fetch
54test('renders users list upon successful API response', async () => {
55 render(<UserDirectory />);
56
57 // Assert loading indicator is present initially
58 expect(screen.getByText(/loading users.../i)).toBeInTheDocument();
59
60 // 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});
66
67// Test 2: Network Error and Retry
68test('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 );
72
73 render(<UserDirectory />);
74
75 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:

  1. Network-Level Interception: MSW intercepts requests at the network layer (using Node's http/https interception in Jest, or Service Workers in browser).
  2. Library-Agnostic: Whether your component uses fetch, Axios, TanStack Query, or XMLHttpRequest, MSW intercepts it transparently.
  3. Declarative Handlers: You define request handlers matching HTTP method and route, returning mocked JSON and status codes.
  4. 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
What is Mock Service Worker (MSW)?

An API mocking library that intercepts HTTP requests at the network level using Service Worker in the browser and Node http interception in unit tests, allowing library-agnostic testing.

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.

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.

Call server.use(rest.get("/api/endpoint", (req, res, ctx) => res(ctx.status(500)))) inside the specific test block to override default successful handlers.

It clears any runtime handler overrides introduced during individual tests, restoring the MSW server to its default configuration defined in setupServer.

Wrap the rendered component in a fresh QueryClientProvider with retry: false configured on QueryClient to prevent test timeouts on error retries.