Community编程与开发github.com

Naki-404/naki-frontend-testing

使用 Vitest 和 React Testing Library 进行前端单元测试和组件测试:关注行为、可访问查询、用户事件、使用 MSW 进行网络模拟、Hooks、异步 UI 以及覆盖率。适用于编写或修复 React 组件、Hooks 或页面的测试,或者当测试不稳定、速度慢或与实现细节耦合时。

naki-frontend-testing 是什么?

这个技能为开发者提供了一套标准化的实践指南,用于在 React 应用中使用 Vitest 和 React Testing Library 编写高效、可靠的前端测试。其核心理念是“测试用户所见和所操作的内容,而非组件的内部实现”,强调通过角色(role)和标签(label)进行可访问性查询,并使用 userEvent 模拟真实用户交互。技能详细说明了如何通过 MSW(Mock Service Worker)在 HTTP 层模拟网络请求,避免直接模拟模块,从而保持测试的鲁棒性。它支持包括 Cursor、Codex 和 Claude Code 在内的主流 AI 代理平台,并提供了从基础设置(如 vitest.setup.ts)、查询速查表、测试工作流到常见错误修复的完整指导。最终目标是帮助开发者编写关注行为、无状态耦合、覆盖关键分支(如错误、空状态)的测试,同时将覆盖率作为信号而非目标。

兼容平台~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),请遵循该技能。

何时使用

  • 需要为新组件、Hook 或页面编写测试。
  • 重构后测试失败,但行为未改变(表明与实现耦合)。
  • 测试不稳定,到处使用 waitFor,或者模拟了半个应用。

核心规则

  1. 像用户一样查询。 优先级:getByRole(配合 name)、getByLabelText、getByPlaceholderText、getByText、getByDisplayValue。仅在没有任何可访问查询可用时使用 getByTestId。
  2. 使用 userEvent 进行交互,而非 fireEvent。使用 const user = userEvent.setup() 创建,并等待每个调用。
  3. 对输出进行断言:可见文本、角色、表单值、对网络端点的调用。绝不对状态变量、Hook 内部实现、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" }) 失败通常意味着真正的可访问性 bug。可以选择为关键屏幕添加 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("列出警报并按严重性筛选", async () => {
  server.use(http.get("/api/alerts", () => HttpResponse.json([
    { id: "a1", title: "VPN 暴力破解", severity: "high" },
    { id: "a2", title: "新管理员登录", severity: "low" },
  ])));
  const user = userEvent.setup();
  render(<AlertList />);

  expect(await screen.findByText("VPN 暴力破解")).toBeInTheDocument();
  await user.selectOptions(screen.getByRole("combobox", { name: "Severity" }), "high");
  expect(screen.queryByText("新管理员登录")).not.toBeInTheDocument();
});

test("加载失败时显示重试按钮", async () => {
  server.use(http.get("/api/alerts", () => new HttpResponse(null, { status: 500 })));
  render(<AlertList />);
  expect(await screen.findByRole("button", { name: /重试/i })).toBeInTheDocument();
});

Hooks 和时间

  • 通过一个使用 Hook 的小组件来测试 Hook,或者当 Hook 没有 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 Hook模拟 HTTP 端点
整个页面的大型快照对关键内容进行显式断言
查询客户端开启了重试,导致错误测试缓慢在测试 Provider 中设置 retry: false
忽略 act 警告使用 findBy* await 异步 UI 更新

Individual skills in this repo

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

相关技能