MSW (Mock Service Worker):前端开发 / 测试时拦截 HTTP 返回 mock

起因

后端 API 还没好,前端要先做 UI;或者测试时不想真打后端(速度 + 隔离)。
传统做法:在 fetch 调用处 if (mocked) return mockData,丑陋且要清理。

MSW 在 service worker / Node 层拦截 HTTP 请求,让"前端代码无修改 +
真的发请求 + 拦截返 mock 数据"。同一套 mock 给开发 / 测试 / Storybook
共用。

解决方案

npm i -D msw
npx msw init public/ --save   # 给浏览器生成 service worker 文件

写 handler

// src/mocks/handlers.ts
import { http, HttpResponse, delay } from 'msw'

export const handlers = [
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Alice',
      email: '[email protected]',
    })
  }),

  http.get('/api/users', async ({ request }) => {
    const url = new URL(request.url)
    const page = Number(url.searchParams.get('page') ?? 1)

    await delay(300)   // 模拟网络延迟

    return HttpResponse.json({
      items: [
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' },
      ],
      page,
      hasMore: page < 5,
    })
  }),

  http.post('/api/login', async ({ request }) => {
    const body = await request.json() as { email: string; pw: string }
    if (body.email === '[email protected]' && body.pw === 'secret') {
      return HttpResponse.json({ token: 'fake-jwt-token' })
    }
    return new HttpResponse(null, { status: 401 })
  }),

  http.delete('/api/posts/:id', async () => {
    await delay(200)
    return new HttpResponse(null, { status: 204 })
  }),
]

浏览器集成(dev)

// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)
// src/main.ts 入口
async function enableMocking() {
  if (import.meta.env.MODE !== 'development') return
  const { worker } = await import('./mocks/browser')
  return worker.start({
    onUnhandledRequest: 'bypass',   // 未 mock 的请求放行
  })
}

enableMocking().then(() => {
  // 启动你的 React / Vue app
  ReactDOM.createRoot(...).render(<App />)
})

npm run dev,所有 fetch / axios 调用被 MSW 拦截返 mock。
Console 显示:

[MSW] GET /api/users/1 (200 OK)
[MSW] GET /api/users?page=1 (200 OK)

DevTools Network 也能看到这些请求(带 [from service worker] 标记)。

Node 集成(Jest / Vitest 测试)

// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// vitest.setup.ts (或 jest.setup.ts)
import { server } from './src/mocks/server'

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

测试代码不需要 mock fetch,直接调真函数:

test('login flow', async () => {
  const { getByLabelText, getByRole } = render(<LoginForm />)
  await userEvent.type(getByLabelText('Email'), '[email protected]')
  await userEvent.type(getByLabelText('Password'), 'secret')
  await userEvent.click(getByRole('button', { name: 'Sign in' }))

  await waitFor(() => {
    expect(localStorage.getItem('token')).toBe('fake-jwt-token')
  })
})

MSW 在 Node 端拦截 fetch(Node 18+ 内置 fetch)/ axios,返 mock 数据。

单测里 override handler

test('shows error on 500', async () => {
  server.use(
    http.get('/api/users', () => {
      return new HttpResponse(null, { status: 500 })
    }),
  )

  const { findByText } = render(<UserList />)
  expect(await findByText(/something went wrong/i)).toBeInTheDocument()
})

server.use(...) 临时覆盖;每个测试 resetHandlers() 清回基准。

Storybook 集成

npm i -D msw-storybook-addon

.storybook/preview.ts

import { initialize, mswLoader } from 'msw-storybook-addon'

initialize()

export default {
  loaders: [mswLoader],
}

每个 story 设特定 handler:

export const ErrorState: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get('/api/users', () => new HttpResponse(null, { status: 500 })),
      ],
    },
  },
}

Storybook 里展示"网络错误"状态而不需要真实坏后端。

E2E (Playwright)

import { test, expect } from '@playwright/test'

test('login', async ({ page }) => {
  await page.route('/api/login', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ token: 'fake' }),
    })
  })

  await page.goto('/login')
  await page.fill('[name=email]', '[email protected]')
  await page.fill('[name=password]', 'secret')
  await page.click('button[type=submit]')
  await expect(page).toHaveURL('/dashboard')
})

Playwright 自己有 page.route 类似机制;用 MSW 时直接复用 handlers
更统一。

动态响应(基于状态)

mock 用户数据库 + 增删改:

let users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

export const handlers = [
  http.get('/api/users', () => HttpResponse.json(users)),
  http.post('/api/users', async ({ request }) => {
    const data = await request.json() as { name: string }
    const u = { id: Date.now(), name: data.name }
    users.push(u)
    return HttpResponse.json(u, { status: 201 })
  }),
  http.delete('/api/users/:id', ({ params }) => {
    users = users.filter(u => String(u.id) !== params.id)
    return new HttpResponse(null, { status: 204 })
  }),
]

前端做 CRUD 流,mock 像真的在工作。

与 OpenAPI 集成

npm i -D openapi-msw
import { createOpenApiMock } from 'openapi-msw'
import type { paths } from './generated-api-types'

const http = createOpenApiMock<paths>()

export const handlers = [
  http.get('/api/users/{id}', () => HttpResponse.json({
    id: 1,
    name: 'Alice',
    // 类型完全跟着 OpenAPI schema,缺字段编译报错
  })),
]

后端给 OpenAPI 文档 → 前端类型 + mock 一起自动有,零手写。

效果

  • 后端 delay 一周时前端可独立开发
  • 单测从"mock fetch 函数"升到"在网络层拦截",更接近真实场景
  • Storybook 演示"loading / error / empty / 满数据"四种状态都有
  • E2E 测试稳定(不依赖真后端 / 不污染数据库)
  • Mock data 文件被 dev / test / Storybook 共用,单一来源

与替代品对比

MSW json-server nock jest.mock('axios')
拦截层 network (sw / node) 真后端 node only 模块 mock
浏览器支持
真发 HTTP ❌(模拟)
灵活程度 极高 中(REST 模板)
Storybook 集成

MSW 全面胜出,是 2024 后的事实标准。

踩过的坑

  1. Service Worker 没注册成功npx msw init 没把 mockServiceWorker.js
    放对位置(应在 publicly served root)。生产 build 后路径变也会失效。

  2. MSW 拦截了真实生产请求:忘了 if (MODE === 'development') 包裹
    worker.start()。线上版本不能注入 MSW。

  3. Node fetch vs cross-fetch:MSW 在 Node 18+ 拦截 global fetch;
    老 Node 用 node-fetch 不被拦截,要装 @mswjs/interceptors

  4. 测试间 handler 泄漏:忘 afterEach(() => server.resetHandlers())
    上个测试 override 的 handler 影响下个测试。

  5. WebSocket 不支持:MSW 主要拦截 HTTP;WebSocket 用 mock-socket
    或专门的 ws mock 库。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。