「後端 API 還沒好,前端開發被卡住了...」這是昨天的會議內容。
「測試環境的 API 又掛了,跑不了測試!」這是今天早上的訊息。
如果你也遇過這些情況,今天要介紹的 MSW(Mock Service Worker)將會是你的救星!
基礎測試 ✅ → 進階測試 ✅ → Kata實戰 ✅ → [框架整合] → 實戰應用
📍 Day 19
MSW 設置與基礎
在前 18 天,我們已經掌握了:
MSW(Mock Service Worker)是一個革命性的 API mocking 工具,它使用 Service Worker 在網路層級攔截請求。
在開發過程中,我們經常遇到這些痛點:
MSW 提供了完美的解決方案!
// ❌ 傳統方式:需要修改實際程式碼
fetch = jest.fn().mockResolvedValue({ data: 'fake' })
// ✅ MSW:不修改程式碼,直接攔截請求
server.use(
http.get('/api/users', () => {
return HttpResponse.json([{ id: 1, name: 'Alice' }])
})
)
npm install --save-dev msw
// 建立 tests/mocks/server.ts
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/greeting', () => {
return HttpResponse.json({ message: 'Hello World' })
}),
]
export const server = setupServer(...handlers)
// 更新 tests/setup.ts
import '@testing-library/jest-dom'
import { server } from './mocks/server'
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' })
})
afterEach(() => {
server.resetHandlers()
})
afterAll(() => {
server.close()
})
// 建立 src/components/Greeting.tsx
import { useState, useEffect } from 'react'
export function Greeting() {
const [greeting, setGreeting] = useState<string>('')
const [loading, setLoading] = useState<boolean>(true)
useEffect(() => {
fetch('/api/greeting')
.then(res => res.json())
.then(data => {
setGreeting(data.message)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
return <div>{greeting}</div>
}
// 建立 tests/day19/greeting.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { Greeting } from '../../src/components/Greeting'
describe('Greeting Component', () => {
test('displays greeting message', async () => {
render(<Greeting />)
expect(screen.getByText('Loading...')).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText('Hello World')).toBeInTheDocument()
})
})
})
MSW 使用 Handler 來定義如何回應特定的 HTTP 請求。
// 建立 tests/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const userHandlers = [
// GET 請求
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
])
}),
// POST 請求
http.post('/api/users', async ({ request }) => {
const newUser = await request.json()
return HttpResponse.json(
{ id: 3, ...newUser },
{ status: 201 }
)
}),
// 帶參數的請求
http.get('/api/users/:id', ({ params }) => {
const { id } = params
return HttpResponse.json({ id, name: `User ${id}` })
}),
]
http.get('/api/data', () => {
return HttpResponse.json(
{ data: 'response' }, // 回應內容
{
status: 200, // 狀態碼
headers: {
'X-Custom': 'value' // Headers
}
}
)
})
MSW 允許我們根據請求內容動態生成回應。
// 建立 tests/day19/dynamic.test.tsx
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
describe('Dynamic Response', () => {
test('respondsBasedOnQuery', async () => {
server.use(
http.get('/api/search', ({ request }) => {
const url = new URL(request.url)
const q = url.searchParams.get('q')
const items = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' }
]
const filtered = q
? items.filter(item =>
item.name.toLowerCase().includes(q.toLowerCase())
)
: []
return HttpResponse.json(filtered)
})
)
const response = await fetch('/api/search?q=app')
const data = await response.json()
expect(data).toHaveLength(1)
expect(data[0].name).toBe('Apple')
})
})
// 建立 tests/mocks/handlers/index.ts
import { userHandlers } from './user'
import { authHandlers } from './auth'
export const handlers = [
...userHandlers,
...authHandlers,
]
// 建立 src/types/api.ts
export interface User {
id: number
name: string
email: string
}
// Handler 中使用
http.get('/api/users', () => {
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' }
]
return HttpResponse.json(users)
})
// 建立 tests/day19/error.test.tsx
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
describe('Error Handling', () => {
test('handlesNetworkError', async () => {
server.use(
http.get('/api/data', () => {
return HttpResponse.error()
})
)
// 測試錯誤處理邏輯
await expect(fetch('/api/data')).rejects.toThrow()
})
test('handlesServerError', async () => {
server.use(
http.get('/api/data', () => {
return HttpResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
)
})
)
const response = await fetch('/api/data')
expect(response.status).toBe(500)
})
})
實作一個簡單的使用者列表,從 /api/users
載入資料並顯示!
MSW 優勢
核心概念
最佳實踐
明天將深入 MSW 進階功能,學習複雜的 API 互動場景!