MSW Mocking
intermediateAPI mocking with Mock Service Worker for tests and development.
testingmockingmswapi
Tested on⬢20▲16⚛19TS5.9
$ bunx sinew add testing/msw-mockingInteractive demo coming soon
1The Problem
Testing components that fetch data is challenging:
- Mocking fetch/axios is brittle and implementation-specific
- Test data is scattered across test files
- Development without a backend requires workarounds
2The Solution
Use MSW to intercept requests at the network level. Same mocks work in tests, Storybook, and development.
3Files
mocks/handlers.ts
mocks/handlers.tsTypeScript
import { http, HttpResponse } from "msw";
export const handlers = [
// GET /api/users
http.get("/api/users", () => {
return HttpResponse.json([
{ id: "1", name: "Alice", email: "alice@example.com" },
{ id: "2", name: "Bob", email: "bob@example.com" },
]);
}),
// GET /api/users/:id
http.get("/api/users/:id", ({ params }) => {
const { id } = params;
if (id === "not-found") {
return HttpResponse.json({ error: "Not found" }, { status: 404 });
}
return HttpResponse.json({
id,
name: "Test User",
email: "test@example.com",
});
}),
// POST /api/users
http.post("/api/users", async ({ request }) => {
const data = await request.json();
return HttpResponse.json({ id: "new-id", ...data }, { status: 201 });
}),
// DELETE /api/users/:id
http.delete("/api/users/:id", ({ params }) => {
return HttpResponse.json({ deleted: params.id });
}),
// Simulate network error
http.get("/api/error", () => {
return HttpResponse.error();
}),
// Simulate slow response
http.get("/api/slow", async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return HttpResponse.json({ data: "slow response" });
}),
];mocks/server.ts
mocks/server.tsTypeScript
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);mocks/browser.ts
mocks/browser.tsTypeScript
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);vitest.setup.ts
vitest.setup.tsTypeScript
import { beforeAll, afterEach, afterAll } from "vitest";
import { server } from "./mocks/server";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());tests/users.test.tsx
tests/users.test.tsxTypeScript
import { render, screen, waitFor } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { server } from "../mocks/server";
import { UserList } from "@/components/user-list";
describe("UserList", () => {
it("displays users from API", async () => {
render(<UserList />);
await waitFor(() => {
expect(screen.getByText("Alice")).toBeInTheDocument();
expect(screen.getByText("Bob")).toBeInTheDocument();
});
});
it("handles empty state", async () => {
server.use(
http.get("/api/users", () => {
return HttpResponse.json([]);
})
);
render(<UserList />);
await waitFor(() => {
expect(screen.getByText("No users found")).toBeInTheDocument();
});
});
it("handles error state", async () => {
server.use(
http.get("/api/users", () => {
return HttpResponse.json({ error: "Server error" }, { status: 500 });
})
);
render(<UserList />);
await waitFor(() => {
expect(screen.getByText("Error loading users")).toBeInTheDocument();
});
});
});app/providers.tsx
app/providers.tsxTypeScript
"use client";
import { useEffect, useState } from "react";
export function Providers({ children }: { children: React.ReactNode }) {
const [mockingEnabled, setMockingEnabled] = useState(false);
useEffect(() => {
async function enableMocking() {
if (process.env.NODE_ENV === "development" && process.env.NEXT_PUBLIC_API_MOCKING === "enabled") {
const { worker } = await import("../mocks/browser");
await worker.start({
onUnhandledRequest: "bypass",
});
setMockingEnabled(true);
} else {
setMockingEnabled(true);
}
}
enableMocking();
}, []);
if (!mockingEnabled) {
return null;
}
return <>{children}</>;
}4Dependencies
$ bun add -D msw5Configuration
Vitest Setup
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],
globals: true,
},
});TypeScript
Enable Browser Mocking
# .env.local
NEXT_PUBLIC_API_MOCKING="enabled"Bash
6Usage
Override Handlers in Tests
import { http, HttpResponse } from "msw";
import { server } from "../mocks/server";
test("specific scenario", async () => {
// Override for this test only
server.use(
http.get("/api/users", () => {
return HttpResponse.json([{ id: "1", name: "Custom User" }]);
})
);
// Test code...
});TypeScript
Dynamic Responses
let callCount = 0;
http.get("/api/data", () => {
callCount++;
if (callCount === 1) {
return HttpResponse.json({ status: "loading" });
}
return HttpResponse.json({ status: "complete", data: [] });
});TypeScript
Request Assertions
import { http, HttpResponse } from "msw";
let capturedRequest: Request | null = null;
server.use(
http.post("/api/users", async ({ request }) => {
capturedRequest = request.clone();
return HttpResponse.json({ id: "1" });
})
);
// After your action
expect(await capturedRequest?.json()).toEqual({
name: "Alice",
email: "alice@example.com",
});TypeScript
7Troubleshooting
Handlers not matching
- Check that the URL pattern matches exactly
- Verify HTTP method is correct
- Use
onUnhandledRequest: "warn"to debug
MSW not working in tests
- Ensure
vitest.setup.tsis insetupFiles - Check that
server.listen()is called inbeforeAll - Verify MSW version compatibility