Communitygithub.com

React 測試

使用 React Testing Library、Vitest/Jest 進行 React 元件測試,搭配 MSW 進行網路層模擬,並利用 axe 進行無障礙斷言,以及明確區分元件測試與 Playwright/Cypress 端對端測試的界線。在撰寫或修正 React 元件、Hook 或頁面的測試時使用。

React 測試 是什麼?

此技能提供了一整套基於行為驅動的 React 測試模式,涵蓋從元件測試、自訂 Hook 測試、無障礙斷言到網路層模擬(Mocking)的最佳實務。核心原則是測試使用者所見與所為,而非實作細節,這確保測試案例更具韌性且更貼近真實使用情境。技能明確指出不同測試執行器(Vitest、Jest、Playwright Component Testing、Cypress Component Testing)的適用場景,並提供查詢優先順序指南、使用者互動(userEvent)的正確使用方式,以及非同步元素與 side effect 的處理模式。特別強調使用 MSW(Mock Service Worker)在網路層進行模擬,讓元件、Hook 與 fetch 函式庫的行為與正式環境完全一致,同時也涵蓋 Provider 包裝、自訂 Hook 測試與無障礙(a11y)斷言等進階主題。此外,技能清楚劃分元件測試(RTL)與端對端測試(Playwright/Cypress)的責任邊界:邏輯相關的元件與表單適合 RTL,而涉及版面、瀏覽器 API 或跨頁面流程則應採用 Playwright/Cypress。最後還提供了涵蓋範圍目標、測試指令與常見反模式,幫助團隊建立穩健且可維護的 React 測試策略。

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

在你喜歡的 AI 中提問

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

說明文件

React 測試 是做什麼的?

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

何時啟用

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

核心原則

測試使用者所見與所為,而非實作細節。

一個測試應該:

  • 使用與正式環境相同的 Provider 渲染元件
  • 透過可存取的查詢(角色、標籤)及 userEvent 進行互動
  • 斷言可見的輸出與可觀察的 side effect(回呼函式被呼叫、請求已發送)

一個測試不應該:

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

函式庫選擇

執行器使用時機備註
VitestVite、Remix、現代專案設定更快、原生 ESM、與 Jest 相容的 API
JestNext.js、CRA、既有專案許多 React 專案的預設選項
Playwright Component Testing需要真實瀏覽器引擎當 JSDOM 缺少必要功能時使用
Cypress Component Testing真實瀏覽器,且已在使用 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("submits the form", 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("Loaded")).toBeInTheDocument();

// Side effect 斷言
await waitFor(() => expect(saveSpy).toHaveBeenCalled());

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

切勿使用 setTimeout 加上斷言——這會造成不穩定的測試。請使用上述的匹配器。

使用 MSW 進行網路模擬

Mock Service Worker 在網路層進行模擬。元件、Hook 和 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("renders error on 500", async () => {
  server.use(
    http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
  );
  render(<UserPage id="1" />);
  expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});

Provider 包裝

將 Provider 包裝在一個 test-utils.tsx 中:

// 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 increments and decrements", () => {
  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 accepts initial value", () => {
  const { result } = renderHook(() => useCounter(10));
  expect(result.current.count).toBe(10);
});

test("useUser fetches user data", 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 has no a11y violations", async () => {
  const { container } = render(<UserCard user={mockUser} />);
  expect(await axe(container)).toHaveNoViolations();
});

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

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

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

何時不該使用快照測試

渲染輸出的快照:

  • 每次樣式變更都會中斷
  • 在 code review 時容易被橡皮圖章式地通過
  • 測試的是實作細節(DOM 結構),而非行為

可接受的快照用途:

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

若要進行元件的視覺回歸測試,請使用 Playwright/Cypress 螢幕截圖或 Percy/Chromatic——實際的視覺差異比對,而非 DOM 字串。

何時使用 Playwright / Cypress

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

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

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

決策邊界:

  • 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。請重構元件
  • 預設情況下模擬子元件 — 應該測試整合,而非隔離。僅在子元件有大量 side effect 時才模擬
  • 忽略 act() 警告 — 它們標示了真正的錯誤(解除掛載後更新狀態、缺少非同步包裝)
  • 跨測試共用可變狀態 — 當測試順序改變時會造成不穩定
  • 移除 it.skip() 後仍可通過的測試 — 你的測試實際上並未如你所想的那樣進行斷言

TDD 工作流程

RED     -> 為下一個需求撰寫會失敗的測試
GREEN   -> 撰寫最簡的元件程式碼使其通過
REFACTOR -> 改善元件,測試保持綠色
REPEAT  -> 下一個需求

對於新元件:

  1. 定義元件的 props 類型和簽章
  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("submits user form and shows success", 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("Name"), "Alice");
  await user.type(screen.getByLabelText("Email"), "[email protected]");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(await screen.findByText(/saved successfully/i)).toBeInTheDocument();
});

測試錯誤邊界

function Broken() {
  throw new Error("boom");
}

test("error boundary renders fallback", () => {
  // 抑制 React 對於預期拋出錯誤的 console.error 雜訊,然後還原,
  // 以免間諜洩漏到其他測試並隱藏真正的錯誤。
  const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
  try {
    render(
      <ErrorBoundary fallback={<div>Something went wrong</div>}>
        <Broken />
      </ErrorBoundary>,
    );

    expect(screen.getByText("Something went wrong")).toBeInTheDocument();
  } finally {
    errorSpy.mockRestore();
  }
});

測試 Suspense 邊界

test("shows loading then content", async () => {
  renderWithProviders(
    <Suspense fallback={<div>Loading...</div>}>
      <UserDetail id="1" />
    </Suspense>,
  );

  expect(screen.getByText("Loading...")).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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相關技能