본문으로 건너뛰기
Previous
Next
MSW Complete Guide | Mock Service Worker for API Testing

MSW Complete Guide | Mock Service Worker for API Testing

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:

  1. Intercepts at network level
  2. Same mocks for dev and tests
  3. Use HttpResponse for responses
  4. Override handlers per test
  5. Support REST and GraphQL

Next Steps:

  • Test with [Vitest](/en/blog/vitest-complete-guide/
  • Use [Jest](/en/blog/jest-complete-guide/
  • E2E with [Playwright](/en/blog/playwright-complete-guide/

Resources:


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Complete MSW guide for API mocking. Learn request handlers, browser/Node.js integration, testing strategies, and buildin… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [Axios Complete Guide](/en/blog/axios-complete-guide/
  • [Jest Complete Guide | JavaScript Testing· Mocking](/en/blog/jest-complete-guide/
  • [Jest Testing Guide | Unit Tests· Mocks](/en/blog/jest-testing-guide/

이 글에서 다루는 키워드 (관련 검색어)

MSW, Testing, API, Mocking, Jest, Vitest, React 등으로 검색하시면 이 글이 도움이 됩니다.