React 測試實戰:
Vitest + Testing Library
unit test、user-event 互動、mock API、coverage — 讓你的元件可測試、可維護、讓 CI 不再擋你路
Section 1:為什麼要寫測試(以及為什麼大家都不寫)
先承認現實。大多數工程師不寫測試,因為三個非常合理的理由:
- 1感覺很費時間 — 「我直接跑起來看一下不就好了?」
- 2不知道要測什麼 — 測元件的 className?還是測業務邏輯?邊界在哪?
- 3測試本身很難維護 — 改了一個 prop 名稱,20 個測試全部失敗
這些都是真實的痛點,但有個更殘酷的真相: 你其實每次改完 code 都在「手動測試」——只是你的手動測試每次都要重做一次。
核心洞察
寫測試是把「手動測試」的步驟自動化。你打開瀏覽器、點幾下、確認畫面——這些步驟可以寫成程式碼, 讓電腦幫你每次 commit 前都執行一遍。
不寫測試的代價,在專案成長後才會真正浮現:
蝴蝶效應
修了一個 bug,結果壞了三個功能。等 QA 或用戶回報才發現。
重構恐懼症
不敢重構,因為不知道會不會壞。技術債越堆越高。
新人地雷區
新人不敢改舊程式碼,只能用複製貼上繞過去。
有了測試之後,重構變得安全,新人可以放心修改,CI 自動幫你把關——這才是測試真正的價值。
Section 2:測試工具選擇
2026 年 React 測試的最佳組合,四個工具各司其職:
Vitest
比 Jest 快 10x,零配置,原生 ESM 支援。Vite 生態系的測試框架, 在 Next.js 專案中也可以獨立使用。
@testing-library/react
模擬用戶行為,不測實作細節。核心哲學:「像用戶一樣使用元件,而不是測試 state 的值」。
@testing-library/user-event
比 fireEvent 更真實的事件模擬。 模擬完整的瀏覽器事件序列,包含 focus、keydown、input、keyup 等。
msw (Mock Service Worker)
攔截 fetch 請求,在 Node 環境中模擬 API。測試 API 整合, 不需要真實 server,也不需要 jest.mock。
安裝所有依賴:
vitest.config.ts — 測試框架設定:
src/test/setup.ts — 全域 setup:
為什麼不用 Jest? Vitest 和 Jest API 幾乎完全相同(所以遷移成本極低), 但 Vitest 利用 Vite 的 HMR 機制,watch mode 下只重跑受影響的測試, 大型專案中速度差異非常明顯。
Section 3:第一個測試 — 渲染測試
Testing Library 的核心哲學
像用戶一樣測試,而不是測試實作細節。 不要測 state 的值、不要測 CSS class name——測「用戶看到什麼」、「用戶可以做什麼」。
UserCard 元件與對應測試:
UserCard.test.tsx:
常用 Query 速查表:
| Query | 用途 | 找不到時 |
|---|---|---|
| getBy* | 找單一元素(確定存在) | 拋出錯誤 |
| queryBy* | 找單一元素(可能不存在) | 回傳 null |
| findBy* | 等待非同步出現 | 拋出錯誤 |
| getAllBy* | 找多個元素 | 拋出錯誤 |
常見搭配:getByText、getByRole、getByPlaceholderText、getByTestId。 優先使用 getByRole,最接近無障礙語義。
Section 4:互動測試 — userEvent
userEvent 比fireEvent 更真實: 它會觸發完整的事件序列(hover → focus → keydown → input → keyup → blur), 更貼近真實瀏覽器行為,也更容易發現潛在問題。
Counter 元件與互動測試:
Counter.test.tsx — 點擊與狀態驗證:
表單輸入測試 — user.type() 模擬鍵盤輸入:
userEvent vs fireEvent: 如果你用 fireEvent.click(), 它只觸發 click 事件。真實的點擊還包含 pointerover、pointerenter、mouseover、mouseenter、 pointermove、mousemove、pointerdown、mousedown、focus、pointerup、mouseup、click。 userEvent 會完整模擬這個序列,讓測試更可靠。
Section 5:Mock API — msw
測試「有 API 請求的元件」是最常見的需求。msw 的做法不是 mock 函數, 而是在 Node 層攔截真實的 fetch 請求,讓元件完全不需要知道自己在被測試。
src/test/handlers.ts — 定義 API mock:
src/test/setup.ts — 啟動 msw server:
UserList.test.tsx — 測試 API 呼叫:
Section 6:Custom Hook 的測試
Hook 不能直接呼叫,需要用renderHook包裝在一個虛擬元件中執行。 修改 Hook 狀態的操作需要用act()包裝,確保 React 的 re-render 完成後再驗證結果。
useCounter Hook 測試:
useLocalStorage Hook 測試 — 需要 mock localStorage:
什麼時候用 renderHook vs render? 如果 Hook 的邏輯可以獨立測試(不依賴 UI),用 renderHook。 如果你想測試「Hook 和 UI 的整合行為」,直接 render一個使用該 Hook 的元件就好。
Section 7:Coverage 與 CI 整合
Coverage 報告告訴你哪些程式碼「被測試跑到過」。 它是輔助工具,不是目標——100% coverage 不等於沒有 bug。
常用指令:
package.json scripts 建議配置:
< 60%
危險
核心業務邏輯可能完全沒有測試保護
80%
合理目標
重要路徑有測試,非核心 UI 可以跳過
100%
不是目標
維護成本可能超過帶來的安全感
Coverage 的優先順序
GitHub Actions CI 整合 — 只需加 3 行:
CI 的真正價值:不是「測試本身」,而是「強制執行」。 沒有 CI,測試可以被跳過;有了 CI,每個 PR 在 merge 前都必須通過測試。 這是讓測試文化在團隊中落地的關鍵機制。
Tags