iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Modern Web

React TDD 實戰:用 Vitest 打造可靠的前端應用系列 第 19

Day 19 - MSW 設置與基礎 🎭

  • 分享至 

  • xImage
  •  

「後端 API 還沒好,前端開發被卡住了...」這是昨天的會議內容。
「測試環境的 API 又掛了,跑不了測試!」這是今天早上的訊息。
如果你也遇過這些情況,今天要介紹的 MSW(Mock Service Worker)將會是你的救星!

測試旅程地圖

基礎測試 ✅ → 進階測試 ✅ → Kata實戰 ✅ → [框架整合] → 實戰應用
                                        📍 Day 19
                                      MSW 設置與基礎

在前 18 天,我們已經掌握了:

  • 測試基礎概念(Day 1-10)
  • TDD 紅綠重構循環
  • Roman Numeral Kata 實戰(Day 11-17)
  • React Testing Library 入門(Day 18)

什麼是 MSW? 🤔

MSW(Mock Service Worker)是一個革命性的 API mocking 工具,它使用 Service Worker 在網路層級攔截請求。

為什麼需要 MSW?

在開發過程中,我們經常遇到這些痛點:

  • 後端 API 尚未完成
  • 測試環境不穩定
  • 無法模擬邊界情況
  • 開發速度被阻塞

MSW 提供了完美的解決方案!

傳統 vs 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

建立 Mock Server

// 建立 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()
})

第一個 MSW 測試

// 建立 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()
    })
  })
})

Request Handlers

MSW 使用 Handler 來定義如何回應特定的 HTTP 請求。

基本 Handler 結構

// 建立 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}` })
  }),

]

Response Resolvers

http.get('/api/data', () => {
  return HttpResponse.json(
    { data: 'response' },     // 回應內容
    { 
      status: 200,            // 狀態碼
      headers: {
        'X-Custom': 'value'   // Headers
      }
    }
  )
})

動態回應

MSW 允許我們根據請求內容動態生成回應。

Query Parameters 處理

// 建立 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')
  })
})

最佳實踐

組織 Handlers

// 建立 tests/mocks/handlers/index.ts
import { userHandlers } from './user'
import { authHandlers } from './auth'

export const handlers = [
  ...userHandlers,
  ...authHandlers,
]

TypeScript 強型別

// 建立 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 優勢

  • 網路層級攔截
  • 不修改程式碼
  • 測試與開發共用

核心概念

  • Request Handlers
  • Response Resolvers
  • 動態回應

最佳實踐

  • 組織化管理
  • TypeScript 型別
  • 錯誤情況測試

預告下一站

明天將深入 MSW 進階功能,學習複雜的 API 互動場景!


上一篇
React Testing Library 入門 🌐
下一篇
Day 20 - 測試 TodoList 元件 📝
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言