Community程式設計與開發github.com

Naki-404/naki-frontend-testing

使用 Vitest 和 React Testing Library 進行前端單元測試與元件測試:專注於行為的測試、可存取的查詢方式、user-event、搭配 MSW 的網路模擬、鉤子、非同步 UI 與涵蓋率。適用於撰寫或修正 React 元件、鉤子或頁面的測試,或當測試不穩定、速度慢或與實作細節耦合時。

naki-frontend-testing 是什麼?

此技能定義了使用 Vitest 和 React Testing Library 進行前端測試的一套標準化方法,重點在於「測試行為而非實作」。它提供嚴格的規則,例如優先使用 `getByRole` 等可存取查詢、透過 `userEvent` 模擬使用者互動、並利用 MSW 在 HTTP 層進行網路模擬,以確保測試的穩定性與可維護性。技能支援 React 元件、鉤子和頁面的測試,並涵蓋渲染狀態(載入、成功、錯誤和空狀態)以及非同步 UI 互動等常見情境。通過遵循「一種行為一個測試」的原則和一個清晰的開發工作流程(列出行為 → 設定 MSW → 撰寫測試 → 驗證失敗),此技能能有效幫助開發者避免測試不穩定、與實作細節耦合的問題,並提升測試套件的可靠性。它也包含了詳盡的設定指南、常見錯誤檢查清單以及處理計時器和日期的實用技巧。

相容平台~Claude Code~Codex CLI~Cursor
Part ofReact
npx skills add https://github.com/Naki-404/naki-claude-kit/tree/HEAD/kit/skills/naki-frontend-testing

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

前端測試

測試使用者能看見和操作的內容,而非元件的建構方式。搭配 naki-tdd 進行迭代循環,使用 naki-e2e-playwright 處理完整瀏覽器流程,並透過 naki-accessibility 確保語意正確。如果專案有專屬的技能規則檔(例如 naki-myapp),請遵循該檔案的指示。

何時使用

  • 需要為新的元件、鉤子或頁面撰寫測試。
  • 重構未改變行為,但測試卻失敗(表示與實作耦合)。
  • 測試不穩定、大量使用 waitFor,或模擬了半個應用程式。

核心規則

  1. 像使用者一樣查詢。 優先級:getByRole(搭配 name)、getByLabelText、getByPlaceholderText、getByText、getByDisplayValue。僅在沒有可存取的查詢方式時,才使用 getByTestId。
  2. 使用 userEvent 進行互動,而非 fireEvent。使用 const user = userEvent.setup() 建立,並對每個呼叫使用 await。
  3. 針對輸出做斷言:可見文字、角色、表單值、對網路端點的呼叫。永遠不要對狀態變數、鉤子內部實作、CSS 類別名稱或元件實例方法做斷言。
  4. 模擬網路,而非你的模組。 在 HTTP 層使用 MSW 處理器。僅對真正的外部邊界(時間、隨機、jsdom 中沒有的瀏覽器 API)才模擬模組。
  5. 對稍後出現的元素使用 findBy*。 保留 waitFor 給非查詢的斷言,且永遠不要在 waitFor 內部放置副作用。
  6. 每個測試測試一種行為,命名為一個句子:shows an error when the server returns 500。
  7. 每個測試設定自己的狀態。 不依賴執行順序;在 afterEach 中重設處理器、計時器和儲存空間。
  8. 透過 renderWithProviders 輔助函式提供真實的 Provider(路由器、關閉重試功能的查詢客戶端、主題),而非模擬它們。
  9. 可存取性是可測試的。 失敗的 getByRole("button", { name: "Save" }) 通常代表一個真實的可存取性錯誤。可選擇性地為關鍵畫面加入 vitest-axe 檢查。
  10. 涵蓋率是訊號,不是目標。 涵蓋重要的分支:錯誤狀態、空狀態、權限。

查詢速查表

元素查詢方式
按鈕getByRole("button", { name: /save/i })
連結getByRole("link", { name: "Docs" })
帶標籤的文字輸入框getByRole("textbox", { name: "Email" }) 或 getByLabelText("Email")
核取方塊getByRole("checkbox", { name: "Remember me" })
標題getByRole("heading", { level: 2, name: "Alerts" })
表格列getAllByRole("row") 然後 within(row)
對話框getByRole("dialog", { name: "Delete alert" })
元素不存在expect(queryByText("Error")).not.toBeInTheDocument()

工作流程

  1. 列出行為:渲染狀態(載入、空、資料、錯誤)和互動。
  2. 為元件呼叫的端點加入 MSW 處理器;預設為成功。
  3. 每個行為撰寫一個測試,對於錯誤情況使用 server.use(...) 覆蓋處理器。
  4. 執行 npx vitest run path/to/file.test.tsx,然後執行整個測試套件。
  5. 刻意讓實作失敗一次(註解掉一行),以確認測試能夠失敗。

設定

// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
import { afterAll, afterEach, beforeAll } from "vitest";
import { cleanup } from "@testing-library/react";
import { server } from "./src/test/server";

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

使用 onUnhandledRequest: "error" 會讓未處理的請求明確失敗,而不是掛起。

範例

import { http, HttpResponse } from "msw";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { server } from "../test/server";
import { AlertList } from "./AlertList";

test("lists alerts and filters by severity", async () => {
  server.use(http.get("/api/alerts", () => HttpResponse.json([
    { id: "a1", title: "Brute force on vpn", severity: "high" },
    { id: "a2", title: "New admin login", severity: "low" },
  ])));
  const user = userEvent.setup();
  render(<AlertList />);

  expect(await screen.findByText("Brute force on vpn")).toBeInTheDocument();
  await user.selectOptions(screen.getByRole("combobox", { name: "Severity" }), "high");
  expect(screen.queryByText("New admin login")).not.toBeInTheDocument();
});

test("shows a retry button when loading fails", async () => {
  server.use(http.get("/api/alerts", () => new HttpResponse(null, { status: 500 })));
  render(<AlertList />);
  expect(await screen.findByRole("button", { name: /retry/i })).toBeInTheDocument();
});

鉤子與時間

  • 透過一個使用該鉤子的小元件來測試鉤子,或在沒有 UI 時使用 renderHook。
  • 模擬計時器:使用 vi.useFakeTimers() 和 userEvent.setup({ advanceTimers: vi.advanceTimersByTime });在 afterEach 中使用 vi.useRealTimers() 恢復。
  • 使用 vi.setSystemTime(new Date("2026-01-01T00:00:00Z")) 凍結日期,使快照和標籤保持穩定。

檢查清單

  • 查詢方式為角色或標籤為基礎。
  • 所有 userEvent 呼叫都已使用 await。
  • 使用 MSW 模擬網路;未處理的請求會失敗。
  • 已涵蓋載入、錯誤和空狀態。
  • 沒有對內部實作或類別名稱做斷言。
  • 測試可獨立執行且不受順序影響。
  • 輸出中沒有 act(...) 警告。

常見錯誤

錯誤修正方法
到處使用 getByTestId使用角色/標籤查詢;若元件缺少標籤則補上
await waitFor(() => getByText("x"))await findByText("x")
使用 fireEvent.change 模擬輸入await user.type(input, "text")
直接模擬 useAlerts 鉤子模擬 HTTP 端點
整個頁面的大型快照對重要的部分進行明確的斷言
查詢客戶端啟用重試功能,導致錯誤測試緩慢在測試 Provider 中設定 retry: false
忽略 act 警告使用 findBy* 等待非同步 UI 更新

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

相關技能