React Testing Mock Service Worker: Setup Guide
Mock Service Worker (MSW) is a library that intercepts HTTP and GraphQL requests at the network layer during tests, allowing your React components to receive mocked responses without connecting to a real backend. Installation requires adding MSW to your dev dependencies, creating a handlers file that defines which requests to intercept and what to return, and configuring your test environment to activate MSW before each test runs.
As a React developer with three years of experience building data-driven applications at Acme Digital, I discovered that many integration test failures stem from brittle mock setups that use shallow endpoint stubs or environment variables. MSW's handler-first approach eliminated those issues on our team's projects.
Why You Need MSW in React Testing
Testing React components that call APIs requires controlling network behavior. Without MSW, you might use jest.mock() on your fetch library, mock the entire window.fetch function, or rely on your backend's test server—all approaches that couple tests to implementation details or external state.
MSW intercepts requests at the network layer (before fetch or axios sends them), allowing you to:
- Return consistent, fast responses in every test run.
- Simulate error conditions (500, 404, timeout) without crashing a real server.
- Log and inspect requests your component actually makes.
- Use the same handlers in your test suite and your Storybook environment.
The library works because HTTP clients in Node.js and the browser use a standard set of APIs (fetch, XMLHttpRequest, axios). MSW patches those APIs at the source, not at the application level.
Step 1: Install MSW and Dependencies
Begin by installing MSW from npm, along with msw/node for test environment integration.
npm install --save-dev msw
Verify installation by checking node_modules/msw exists:
ls node_modules/msw
You should see a directory tree with lib/, dist/, and package.json. No additional peer dependencies are required for React testing.
Step 2: Create Your Handlers File
Handlers are functions that define which API endpoints MSW should intercept and what responses to return. Create a file src/mocks/handlers.js to hold your first handler set.
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/users", () => {
return HttpResponse.json(
[
{ id: 1, name: "Alice Chen", email: "[email protected]" },
{ id: 2, name: "Bob Ramirez", email: "[email protected]" },
],
{ status: 200 }
);
}),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{ id: 3, ...body, created: true },
{ status: 201 }
);
}),
];
Each handler uses http.get() or http.post() to specify the method and URL pattern. The callback receives a request object and must return an HttpResponse with a JSON body and HTTP status. This pattern mirrors real-world API contracts and makes tests self-documenting.
Step 3: Create a Test Server
MSW needs a setup file that registers handlers and activates the interceptor. Create src/mocks/server.js:
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
The setupServer() function creates a test server instance that will intercept requests matching your handlers. The spread operator (...handlers) unpacks your handler array.
Step 4: Configure Your Test Environment
Most React projects use Jest or Vitest as a test runner. Configure MSW to start before all tests and reset between test cases.
If using Jest, add this to your test setup file (or create one at src/setupTests.js):
import { server } from "./mocks/server";
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
If you do not already have a setup file, add it to your jest.config.js (or package.json):
// jest.config.js
module.exports = {
setupFilesAfterEnv: ["<rootDir>/src/setupTests.js"],
};
For Vitest, add to your vitest.config.js:
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
setupFiles: ["./src/setupTests.js"],
},
});
The beforeAll() hook starts MSW listening before any test runs. afterEach() resets handlers to a clean state (removing any per-test overrides). afterAll() stops the server when all tests finish.
Step 5: Write Your First Test
Create a test file src/api.test.js that uses your mocked handler:
import { render, screen, waitFor } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { server } from "./mocks/server";
function UserList() {
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
fetch("/api/users")
.then((res) => res.json())
.then(setUsers);
}, []);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
test("renders users from mocked API", async () => {
render(<UserList />);
const alice = await screen.findByText("Alice Chen");
expect(alice).toBeInTheDocument();
});
test("handles custom response per test", async () => {
server.use(
http.get("/api/users", () => {
return HttpResponse.json([{ id: 99, name: "Override User" }]);
})
);
render(<UserList />);
const override = await screen.findByText("Override User");
expect(override).toBeInTheDocument();
});
The first test runs with the default handlers. The second test calls server.use() to override the handler for that test only. After afterEach(), the handler resets to the original.
Verifying Your Setup
Run your tests to confirm MSW is intercepting correctly:
npm test
You should see both tests pass, and you should not see network errors or CORS warnings. If requests fail, verify:
- Handler URLs match your fetch calls exactly (case-sensitive).
- HTTP method (
get,post) matches the request method. setupTests.jsis listed in yoursetupFilesAfterEnvorsetupFiles.
Common Pitfall: Forgettig the Response Status
MSW handlers must return an HttpResponse with both a body and a status. Omitting the status defaults to 200, which can mask bugs where your component checks response.status or expects an error. Always be explicit.
Key Takeaways
- MSW intercepts network requests before they leave the browser or Node.js, allowing consistent mocking without external dependencies.
- Handlers are defined in a central file, making your mock API contract visible and reusable.
setupServer()creates a test server;beforeAll(),afterEach(), andafterAll()lifecycle hooks manage its lifecycle.server.use()overrides handlers for specific tests, allowing fine-grained control without global state pollution.- Explicit HTTP status codes in responses prevent tests from silently passing when they should fail.
Frequently Asked Questions
What is the difference between MSW and jest.mock()?
jest.mock() replaces module imports with mocks at the JavaScript level, requiring you to know the exact fetch/axios internals. MSW intercepts at the network layer, so it works with any HTTP client and mirrors real API behavior more closely. MSW is ideal for integration tests; jest.mock() is better for unit tests of helper functions.
Do I need to modify my fetch calls for MSW to work?
No. MSW patches native fetch and XMLHttpRequest at the global level during tests. Your component code does not need to know MSW exists. This makes MSW invisible to your application logic.
Can MSW mock GraphQL requests?
Yes. MSW supports GraphQL via graphql.query() and graphql.mutation() handlers. See the series article on testing GraphQL queries for a complete example.
Why do I see "Failed to fetch" errors even with MSW set up?
The most common cause is a URL mismatch. Handler URLs are case-sensitive and must match your fetch call exactly. Use the DevTools Network tab to confirm the fetch URL, then verify your handler pattern matches it.