Mock Service Worker (MSW): Giả lập API cho Frontend, Hết chờ đợi Backend

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Câu chuyện quen thuộc của mọi frontend developer

Bạn đang làm tính năng mới, backend chưa xong API, hoặc API đang bị lỗi trên server dev. Kết quả: ngồi chờ, hoặc viết code if (isDev) return fakeData rải khắp nơi rồi quên không xóa trước khi deploy. Nghe quen không?

Mình đã ở trong tình huống đó nhiều lần. Team mình từng có sprint mà frontend xong sớm 3 ngày nhưng không test được gì vì API chưa sẵn sàng. Ba ngày lãng phí hoàn toàn. Đó là lúc mình bắt đầu tìm cách giải quyết bài toán này bài bản hơn — và Mock Service Worker (MSW) là câu trả lời.

MSW là gì và tại sao nó khác với các giải pháp mock thông thường

MSW là thư viện JavaScript cho phép bạn chặn các HTTP request ngay tại tầng mạng — không phải mock ở tầng code. Nghe đơn giản, nhưng đây là điểm mấu chốt.

Các cách mock API truyền thống thường làm một trong hai việc:

  • Mock trực tiếp trong code: jest.mock('./api') — chỉ dùng được trong test, không dùng được khi dev thực tế
  • Dựng một server fake riêng (json-server, Express mock) — cần cài thêm, cấu hình thêm, maintain thêm

MSW đi theo hướng khác hẳn. Nó cài một Service Worker vào trình duyệt — hoặc dùng Node.js interceptor trong môi trường test. Worker này nằm giữa app của bạn và mạng thật, bắt từng request và trả về response giả lập theo logic bạn định nghĩa.

Ứng dụng của bạn không biết mình đang nhận data giả. Nó gọi fetch() bình thường, nhận response bình thường. Quan trọng hơn: cùng một bộ handler dùng được cả trong môi trường dev lẫn test suite — không cần viết hai lần.

Cài đặt và cấu hình MSW

1. Cài thư viện

npm install msw --save-dev

2. Định nghĩa handlers

Handlers là nơi bạn khai báo “nếu app gọi endpoint X thì trả về Y”. Tạo file src/mocks/handlers.js:

import { http, HttpResponse } from 'msw'

export const handlers = [
  // GET /api/users — trả về danh sách user giả
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Nguyễn Văn A', role: 'admin' },
      { id: 2, name: 'Trần Thị B', role: 'user' },
    ])
  }),

  // POST /api/login — giả lập đăng nhập
  http.post('/api/login', async ({ request }) => {
    const body = await request.json()
    if (body.username === 'admin' && body.password === '1234') {
      return HttpResponse.json({ token: 'fake-jwt-token', success: true })
    }
    return HttpResponse.json(
      { message: 'Sai tài khoản hoặc mật khẩu' },
      { status: 401 }
    )
  }),

  // GET /api/products/:id — dynamic route param
  http.get('/api/products/:id', ({ params }) => {
    const { id } = params
    return HttpResponse.json({
      id,
      name: `Sản phẩm #${id}`,
      price: 99000,
    })
  }),
]

Trước khi paste JSON response vào handler, mình hay dùng toolcraft.app/vi/tools/developer/json-formatter để format và validate cấu trúc nhanh — tiện hơn nhiều so với cài extension trên browser.

3. Setup cho môi trường trình duyệt (dev server)

Chạy lệnh sau để MSW tạo file Service Worker:

npx msw init public/ --save

Lệnh này tạo file public/mockServiceWorker.js — đây là worker script thật sự chạy trong browser.

Tạo file src/mocks/browser.js:

import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)

Kích hoạt trong src/main.js (hoặc index.js):

async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') return
  const { worker } = await import('./mocks/browser')
  return worker.start()
}

enableMocking().then(() => {
  // mount app vào đây
  ReactDOM.createRoot(document.getElementById('root')).render(<App />)
})

Chạy npm run dev và mở DevTools, bạn sẽ thấy dòng log: “[MSW] Mocking enabled”. Từ lúc này mọi request khớp với handler đều bị intercept — không có gì ra ngoài internet cả.

4. Setup cho unit test (Vitest / Jest)

Trong Node.js không có Service Worker, nên MSW dùng HTTP interceptor riêng. Tạo file src/mocks/server.js:

import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)

Trong file setup của Vitest (src/setupTests.js):

import { server } from './mocks/server'
import { afterAll, afterEach, beforeAll } from 'vitest'

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())  // reset sau mỗi test
afterAll(() => server.close())

Và trong vite.config.js:

test: {
  setupFiles: './src/setupTests.js',
}

Viết test thực tế với MSW

Xem thực tế test trông như thế nào khi dùng MSW. Đây là ví dụ test component React gọi API:

import { render, screen, waitFor } from '@testing-library/react'
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
import UserList from './UserList'

test('hiển thị danh sách user', async () => {
  render(<UserList />)
  // Chờ data load xong
  await waitFor(() => {
    expect(screen.getByText('Nguyễn Văn A')).toBeInTheDocument()
  })
})

test('hiển thị thông báo lỗi khi API fail', async () => {
  // Override handler chỉ cho test này
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json(
        { message: 'Internal Server Error' },
        { status: 500 }
      )
    })
  )
  render(<UserList />)
  await waitFor(() => {
    expect(screen.getByText(/đã có lỗi xảy ra/i)).toBeInTheDocument()
  })
})

Test case thứ hai dùng server.use() để override handler chỉ trong scope của test đó. Sau test xong, afterEach(() => server.resetHandlers()) tự rollback về handler mặc định — không cần cleanup thủ công.

Mẹo dùng MSW hiệu quả hơn

Delay response để test loading state

import { delay } from 'msw'

http.get('/api/users', async () => {
  await delay(1500)  // giả lập mạng chậm 1.5 giây
  return HttpResponse.json([...])
})

Dùng passthrough để cho một số request đi qua thật

import { passthrough } from 'msw'

http.get('/api/public-data', () => {
  return passthrough()  // không mock — request đi thẳng ra server thật
})

Chia nhỏ handlers theo feature

// handlers/users.js
export const userHandlers = [http.get('/api/users', ...), http.post('/api/users', ...)]

// handlers/products.js
export const productHandlers = [http.get('/api/products', ...)]

// handlers/index.js
export const handlers = [...userHandlers, ...productHandlers]

Cách này giúp handlers dễ tìm, dễ bảo trì khi dự án lớn dần.

Từ công cụ tiện lợi thành thói quen tốt

Sau khi tích hợp MSW, mình viết test nhiều hơn hẳn. Lý do đơn giản: test không còn phụ thuộc vào việc server có chạy không, API có sẵn không. Bật máy lên, chạy npm test là có kết quả ngay.

Khi backend thay đổi API contract, chỉ cần cập nhật handlers một chỗ. Cả môi trường dev lẫn test đều được update theo. Không còn tình trạng test pass nhưng UI thật bị lỗi vì data structure đã thay đổi.

MSW không thay thế integration test với API thật — cái đó vẫn cần. Nhưng nó giải quyết đúng vấn đề mà mình gặp hàng ngày: dev nhanh hơn, test dễ hơn, frontend và backend không chờ nhau nữa. Đặc biệt hữu ích trong team có sự tách biệt rõ giữa hai phía.

Share: