MSW Complete Guide | Mock Service Worker for API Testing
이 글의 핵심
MSW (Mock Service Worker) intercepts requests at the network level using Service Workers. It provides seamless API mocking for development and testing.
Introduction
MSW (Mock Service Worker) is an API mocking library that intercepts requests at the network level. Unlike traditional mocking, MSW uses Service Workers to intercept actual HTTP requests.
Traditional Mocking
// Tightly coupled to implementation
jest.mock('axios');
axios.get.mockResolvedValue({ data: { name: 'Alice' } });
Problems:
- ❌ Coupled to HTTP library
- ❌ Different mocks for fetch vs axios
- ❌ Doesn’t work in browser
MSW
// Intercepts at network level
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.json({ name: 'Alice' }));
});
Benefits:
- ✅ Library-agnostic (fetch, axios, etc.)
- ✅ Works in browser AND Node.js
- ✅ Same mocks for dev and tests
1. Installation
npm install --save-dev msw
Browser Setup
npx msw init public/ --save
This creates public/mockServiceWorker.js.
2. Defining Handlers
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
// GET request
http.get('/api/user', () => {
return HttpResponse.json({
id: 1,
name: 'Alice',
email: '[email protected]',
});
}),
// POST request
http.post('/api/login', async ({ request }) => {
const { email, password } = await request.json();
if (email === '[email protected]' && password === 'password') {
return HttpResponse.json({ token: 'fake-token' });
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}),
// Dynamic path
http.get('/api/users/:id', ({ params }) => {
const { id } = params;
return HttpResponse.json({ id, name: `User ${id}` });
}),
];
3. Browser Integration
// mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// main.tsx
import { worker } from './mocks/browser';
if (import.meta.env.DEV) {
worker.start({
onUnhandledRequest: 'warn',
});
}
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
Now all API requests are mocked in development!
4. Node.js Integration (Tests)
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// tests/setup.ts (Jest/Vitest)
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from '../mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
5. Testing with MSW
// UserProfile.tsx
export function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
// UserProfile.test.tsx
import { render, screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';
test('displays user name', async () => {
render(<UserProfile userId={1} />);
expect(await screen.findByText('User 1')).toBeInTheDocument();
});
6. Response Modifiers
import { http, HttpResponse, delay } from 'msw';
export const handlers = [
// Delayed response
http.get('/api/slow', async () => {
await delay(2000);
return HttpResponse.json({ message: 'Slow response' });
}),
// Error response
http.get('/api/error', () => {
return HttpResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}),
// Network error
http.get('/api/network-error', () => {
return HttpResponse.error();
}),
// Custom headers
http.get('/api/with-headers', () => {
return HttpResponse.json(
{ data: 'value' },
{
headers: {
'X-Custom-Header': 'custom-value',
'Cache-Control': 'no-cache',
},
}
);
}),
];
7. Runtime Request Handlers
// Override handler for specific test
import { server } from '../mocks/server';
import { http, HttpResponse } from 'msw';
test('handles server error', async () => {
server.use(
http.get('/api/user', () => {
return HttpResponse.json(
{ error: 'Server error' },
{ status: 500 }
);
})
);
// Test error handling...
});
8. GraphQL Support
import { graphql, HttpResponse } from 'msw';
export const handlers = [
graphql.query('GetUser', ({ variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.id,
name: 'Alice',
},
},
});
}),
graphql.mutation('CreatePost', ({ variables }) => {
return HttpResponse.json({
data: {
createPost: {
id: Date.now(),
title: variables.title,
},
},
});
}),
];
9. Real-World Example: E-commerce API
// mocks/handlers.ts
import { http, HttpResponse, delay } from 'msw';
interface Product {
id: number;
name: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 29 },
{ id: 3, name: 'Keyboard', price: 79 },
];
let cart: { productId: number; quantity: number }[] = [];
export const handlers = [
// Get products
http.get('/api/products', async () => {
await delay(500);
return HttpResponse.json({ products });
}),
// Get product by ID
http.get('/api/products/:id', ({ params }) => {
const product = products.find(p => p.id === Number(params.id));
if (!product) {
return HttpResponse.json(
{ error: 'Product not found' },
{ status: 404 }
);
}
return HttpResponse.json({ product });
}),
// Add to cart
http.post('/api/cart', async ({ request }) => {
const { productId, quantity } = await request.json();
const existing = cart.find(item => item.productId === productId);
if (existing) {
existing.quantity += quantity;
} else {
cart.push({ productId, quantity });
}
return HttpResponse.json({ cart }, { status: 201 });
}),
// Get cart
http.get('/api/cart', () => {
return HttpResponse.json({ cart });
}),
// Clear cart
http.delete('/api/cart', () => {
cart = [];
return HttpResponse.json({ success: true });
}),
];
10. Best Practices
1. Share Handlers Between Dev and Tests
mocks/
├── handlers.ts # Shared handlers
├── browser.ts # Browser setup
└── server.ts # Node.js setup
2. Use Realistic Data
// Good: realistic
http.get('/api/user', () => {
return HttpResponse.json({
id: 1,
name: 'Alice Johnson',
email: '[email protected]',
createdAt: '2024-01-15T10:30:00Z',
});
});
// Bad: minimal
http.get('/api/user', () => {
return HttpResponse.json({ name: 'test' });
});
3. Test Error States
test('handles network error', async () => {
server.use(
http.get('/api/user', () => {
return HttpResponse.error();
})
);
render(<UserProfile />);
expect(await screen.findByText('Network error')).toBeInTheDocument();
});
11. Debugging
// Enable detailed logging
worker.start({
onUnhandledRequest: 'warn',
onUnhandledResponse: 'warn',
});
// Log all requests
http.get('/api/*', ({ request }) => {
console.log('Request:', request.method, request.url);
return HttpResponse.json({});
});
Summary
MSW revolutionizes API mocking:
- Network-level interception
- Works everywhere - browser, Node.js
- Library-agnostic - fetch, axios, etc.
- Realistic - uses actual HTTP
- GraphQL support built-in
Key Takeaways:
- Intercepts at network level
- Same mocks for dev and tests
- Use HttpResponse for responses
- Override handlers per test
- Support REST and GraphQL
Next Steps:
- Test with Vitest
- Use Jest
- E2E with Playwright
Resources: