Communitygithub.com

React 測試

使用 React Testing Library、Vitest/Jest、MSW 進行網路模擬、axe 進行無障礙斷言,並判斷元件測試與 Playwright/Cypress 端到端測試的界線。適用於編寫或修復 React 元件、Hook 或頁面的測試。

React 測試 是什麼?

這個技能提供了一套全面的 React 測試模式,著重於以行為為導向的元件測試、自訂 Hook 測試、無障礙斷言以及網路層級的模擬。它指導開發者使用 React Testing Library 和 Vitest/Jest 作為主要測試框架,並搭配 MSW 攔截 HTTP 請求。核心原則是測試使用者能看到和操作的行為,而非實作細節。技能詳細說明了查詢優先級(如 getByRole)、使用者互動(userEvent)、非同步模式、Provider 封裝、以及自訂 Hook 測試的最佳實踐。此外,它還涵蓋了使用 axe 進行無障礙斷言、快照測試的適用時機、以及何時應轉向 Playwright/Cypress 進行真實瀏覽器測試。技能明確界定了不同測試層的覆蓋率目標,並列出了應避免的反模式。最後,它提供了一個 TDD 工作流程和具體的測試指令,幫助開發者從頭建立穩固的測試策略。

相容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/react-testing

在你喜歡的 AI 中提問

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

說明文件

React 測試 是做什麼的?

全面的 React 測試模式,專注於行為導向的元件測試、自訂 Hook 測試、無障礙斷言以及網路層級的模擬。

啟用時機

  • 為 React 元件、自訂 Hook 或頁面編寫測試
  • 為舊的、未經測試的元件增加測試覆蓋率
  • 從 Enzyme 或類元件時代的模式遷移到 React Testing Library
  • 為新的 React 專案設定 Vitest 或 Jest
  • 在測試中模擬 HTTP 請求
  • 斷言無障礙違規
  • 決定哪些測試屬於 RTL、Playwright 元件測試還是完整的 E2E 測試

核心原則

測試使用者看到和操作的行為,而不是實作細節。

一個測試應該:

  • 使用與生產環境相同的 Provider 來渲染元件
  • 透過可存取的查詢方式(role, label)和 userEvent 與元件互動
  • 斷言可見的輸出和可觀察的副作用(回呼被呼叫、請求被發送)

一個測試不應該:

  • 檢查元件的 state、傳遞給子元件的 props,或哪些 hooks 被呼叫
  • 模擬 React 本身或框架的 hooks
  • 斷言渲染次數或超出使用者影響範圍的 DOM 結構

測試函式庫選擇

執行器使用時機備註
VitestVite, Remix, 現代化設定更快、原生 ESM、與 Jest 相容的 API
JestNext.js, CRA, 既有專案許多 React 專案的預設選擇
Playwright 元件測試需要真實瀏覽器引擎當 JSDOM 缺乏必要功能時使用
Cypress 元件測試需要真實瀏覽器,且已在使用 CypressPlaywright CT 的替代方案

選擇一個即可。除非有明確的職責分離,否則不要在同一个倉庫中同時執行 RTL + Vitest 和 Playwright CT。

查詢優先級

React Testing Library 提供了三個層級的查詢方式 — 請由上而下使用:

  1. 所有人都可存取:getByRole、getByLabelText、getByPlaceholderText、getByText、getByDisplayValue
  2. 語意化:getByAltText、getByTitle
  3. 測試 ID(逃生門):getByTestId
// 最佳
screen.getByRole("button", { name: /save/i });

// 對輸入框來說可以
screen.getByLabelText("Email");

// 最後手段
screen.getByTestId("save-btn");

變體:

  • getBy* — 若無匹配則拋出錯誤
  • queryBy* — 返回 null(用於「斷言不存在」)
  • findBy* — 非同步,返回一個 Promise(用於非同步操作後出現的元素)

使用 userEvent 進行使用者互動

import userEvent from "@testing-library/user-event";

test("提交表單", async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<UserForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText("Email"), "[email protected]");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(onSubmit).toHaveBeenCalledWith({ email: "[email protected]" });
});
  • 務必 await userEvent 的呼叫
  • 每個測試呼叫一次 userEvent.setup(),並重複使用回傳的 user
  • userEvent 模擬真實的瀏覽器事件序列;fireEvent 則只觸發單一合成事件 — 優先使用 userEvent

非同步模式

// 非同步工作後出現的元素
expect(await screen.findByText("已載入")).toBeInTheDocument();

// 副作用斷言
await waitFor(() => expect(saveSpy).toHaveBeenCalled());

// 應該消失的元素
await waitForElementToBeRemoved(() => screen.queryByText("載入中"));

永遠不要使用 setTimeout + 斷言 — 這會導致測試不穩定。請使用上述的匹配器。

使用 MSW 進行網路模擬

Mock Service Worker 在網路層級進行模擬。元件、Hooks 和 fetch 函式庫的行為都與生產環境完全相同。

設定

// test/setup.ts
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users/:id", ({ params }) =>
    HttpResponse.json({ id: params.id, name: "Alice" }),
  ),
  http.post("/api/users", async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: "new-id", ...body }, { status: 201 });
  }),
];

export const server = setupServer(...handlers);

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

設定 onUnhandledRequest: "error",讓任何未模擬的請求在測試中明確失敗 — 靜默通過比測試紅字更糟糕。

每個測試的覆寫

test("在 500 錯誤時渲染錯誤訊息", async () => {
  server.use(
    http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
  );
  render(<UserPage id="1" />);
  expect(await screen.findByText(/發生錯誤/i)).toBeInTheDocument();
});

Provider 封裝

在 test-utils.tsx 中一次性封裝所有 Provider:

// test-utils.tsx
import { render, RenderOptions } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

export function renderWithProviders(
  ui: React.ReactElement,
  options?: RenderOptions,
) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });

  return render(
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={lightTheme}>
        <MemoryRouter>{ui}</MemoryRouter>
      </ThemeProvider>
    </QueryClientProvider>,
    options,
  );
}

export * from "@testing-library/react";

然後在每個測試檔案中 import { renderWithProviders, screen } from "test-utils"。

自訂 Hook 測試

import { renderHook, act } from "@testing-library/react";

test("useCounter 可以增加和減少", () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);
});

test("useCounter 可以接受初始值", () => {
  const { result } = renderHook(() => useCounter(10));
  expect(result.current.count).toBe(10);
});

test("useUser 取得使用者資料", async () => {
  // 每次測試在 wrapper 外部建立一次 QueryClient,使其可以在重新渲染時存活。
  // 在 wrapper 閉包內建立它會導致每次渲染時重置快取狀態,產生不穩定的測試。
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  const { result } = renderHook(() => useUser("1"), { wrapper });

  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data).toEqual({ id: "1", name: "Alice" });
});
  • 將改變狀態的呼叫包裹在 act 中
  • 僅透過 Hook 的公開 API 進行測試
  • 對於使用 context 的 Hook,請傳入 wrapper

無障礙斷言

import { axe, toHaveNoViolations } from "jest-axe"; // 或 vitest-axe
expect.extend(toHaveNoViolations);

test("UserCard 沒有無障礙違規", async () => {
  const { container } = render(<UserCard user={mockUser} />);
  expect(await axe(container)).toHaveNoViolations();
});

在元件測試中對每個互動元件執行 axe。可以檢測:

  • 表單輸入缺少標籤
  • 無效的 ARIA 使用
  • 顏色對比度不足(有限制 — JSDOM 沒有真正的 CSS 引擎,因此僅對內聯樣式有效;視覺對比度屬於 Playwright 的範疇)
  • 圖片缺少替代文字
  • 標題順序違規

交叉連結:skills/accessibility/SKILL.md 了解更廣泛的無障礙測試手冊。

何時不該使用快照測試

渲染輸出的快照:

  • 每次樣式變更都會中斷
  • 在程式碼審查時常被草率通過
  • 測試的是實作細節(DOM 結構),而非行為

可接受的快照使用場景:

  • 純資料序列化函式(formatInvoice(invoice) -> 穩定字串)
  • 生成的設定檔(例如 webpack 設定輸出)

對於元件的視覺回歸測試,請使用 Playwright/Cypress 的螢幕截圖或 Percy/Chromatic — 真正的視覺差異比對,而非 DOM 字串。

何時使用 Playwright / Cypress

JSDOM(由 Vitest/Jest 使用)無法:

  • 渲染真實佈局(flexbox, grid, viewport 查詢)
  • 執行原生瀏覽器動畫、CSS 過渡效果
  • 測試滾動行為、拖放、從剪貼簿貼上
  • 處理 iframe、彈出視窗、下載、跨域流程
  • 在受控環境中運行真實網路並支援完整的 DevTools

對於上述任何情況,請使用 Playwright 元件測試(在真實瀏覽器中測試元件)或完整的 E2E 測試。請參閱 e2e-testing skill。

決策邊界:

  • Hook、純展示元件、包含邏輯的表單 -> RTL
  • 佈局至關重要或使用 JSDOM 未提供的瀏覽器 API 的元件 -> Playwright CT
  • 跨越多個頁面的完整使用者流程 -> Playwright/Cypress E2E

覆蓋率目標

層級目標
純工具函式>=90%
自訂 Hook>=85%
展示元件>=80% — 行為,而非程式碼行數
容器元件>=70% — 主要路徑 + 錯誤狀態
頁面E2E 獨立覆蓋;最少要進行冒煙測試

透過 vitest.config.ts / jest.config.js 設定:

// vitest.config.ts
test: {
  coverage: {
    provider: "v8",
    reporter: ["text", "html", "lcov"],
    thresholds: {
      lines: 80,
      functions: 80,
      branches: 70,
      statements: 80,
    },
  },
}

反模式

  • container.querySelector("...") — 繞過無障礙查詢,讓測試在真實使用者會失敗的情況下通過
  • 斷言渲染次數 — 這是實作細節
  • jest.mock("react", ...) — 永遠不要模擬 React。請重構元件
  • 預設模擬子元件 — 測試的是整合,而非隔離。僅在子元件有嚴重副作用時才進行模擬
  • 忽略 act() 警告 — 它們標示了真實的錯誤(unmount 後的狀態更新、缺少非同步包裹)
  • 跨測試共享可變狀態 — 當測試順序改變時會導致不穩定
  • 移除 it.skip() 後通過的測試 — 你的測試並未真正斷言你認為的內容

TDD 工作流程

紅燈     -> 為下一個需求編寫失敗的測試
綠燈     -> 編寫最少的元件程式碼使其通過
重構     -> 改進元件,測試保持綠燈
重複     -> 下一個需求

對於新元件:

  1. 定義元件的 prop 類型和簽章
  2. 為最簡單的情況編寫第一個測試
  3. 驗證測試因正確的原因失敗
  4. 實作足以通過的程式碼
  5. 加入下一個測試案例
  6. 當第三個類似的測試揭示了某種模式時進行重構

測試指令

# Vitest
vitest                            # 監聽模式
vitest run                        # 單次執行
vitest run --coverage             # 搭配覆蓋率
vitest run path/to/file.test.tsx  # 單一檔案

# Jest
jest --watch
jest --coverage
jest path/to/file.test.tsx

# CI 模式
CI=true vitest run --coverage

相關資源

範例

使用 MSW 和 userEvent 進行表單提交

test("提交使用者表單並顯示成功訊息", async () => {
  server.use(
    http.post("/api/users", () =>
      HttpResponse.json({ id: "1", name: "Alice" }, { status: 201 }),
    ),
  );

  const user = userEvent.setup();
  renderWithProviders(<UserForm />);

  await user.type(screen.getByLabelText("姓名"), "Alice");
  await user.type(screen.getByLabelText("Email"), "[email protected]");
  await user.click(screen.getByRole("button", { name: /儲存/i }));

  expect(await screen.findByText(/儲存成功/i)).toBeInTheDocument();
});

測試錯誤邊界

function Broken() {
  throw new Error("爆炸");
}

test("錯誤邊界渲染備用 UI", () => {
  // 抑制 React 對於預期拋出的 console.error 雜訊,然後恢復,
  // 這樣 spy 就不會洩漏到測試之外並隱藏其他地方的真正錯誤。
  const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
  try {
    render(
      <ErrorBoundary fallback={<div>發生錯誤</div>}>
        <Broken />
      </ErrorBoundary>,
    );

    expect(screen.getByText("發生錯誤")).toBeInTheDocument();
  } finally {
    errorSpy.mockRestore();
  }
});

測試 Suspense 邊界

test("顯示載入中然後顯示內容", async () => {
  renderWithProviders(
    <Suspense fallback={<div>載入中...</div>}>
      <UserDetail id="1" />
    </Suspense>,
  );

  expect(screen.getByText("載入中...")).toBeInTheDocument();
  expect(await screen.findByText("Alice")).toBeInTheDocument();
});

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

相關技能