3.5 测试 Testing


3.5 测试(Testing)

本节摘要:应用越大越不敢改——测试就是"改代码的勇气来源"。本节讲清 Next.js 测试体系:单元测试(Vitest)、组件测试(Testing Library)、端到端测试(Playwright),以及"测试什么、怎么组织"的策略。

上手前先明确

阅读完本节,你应当能够:

  1. 用 Vitest 写单元测试
  2. 用 Testing Library 测试组件
  3. 用 Playwright 做端到端测试
  4. 理解测试金字塔与投入分配
  5. 在 CI 中跑测试

问题与直觉:为什么测试是"工程化"的标配

功能能跑 ≠ 能维护。没有测试的代码库:改 A 功能,B 悄悄坏了,没人发现。测试的价值不是"证明现在对",而是"以后敢改"——有了测试网,重构、升级依赖、加功能都不怕。

直觉类比:测试像"安全网"——走钢丝(改代码)的人,有网(测试)才敢做动作。没有网,只能原地不动(不敢改),或摔下去(上线出 bug)。

💡 关键直觉:测试分层投入——纯逻辑用单元测试(最多、最快),组件/页面用组件测试,核心用户流程用端到端测试(最少、最贵)。把预算花在"容易坏且坏了代价高"的地方。

核心原理:三层测试

2.1 单元测试(Vitest)

测纯函数与逻辑(工具函数、数据格式化、字典处理):

npm install -D vitest
// utils/format.test.ts import { describe, expect, it } from "vitest"; import { formatDate } from "./format"; describe("formatDate", () => { it("中文格式", () => { expect(formatDate(new Date("2026-08-15"), "zh")).toBe("2026年8月15日"); }); it("英文格式", () => { expect(formatDate(new Date("2026-08-15"), "en")).toBe("August 15, 2026"); }); });

2.2 组件测试(Testing Library)

测组件渲染与交互:

npm install -D @testing-library/react @testing-library/jest-dom jsdom
// components/LikeButton.test.tsx import { render, screen, fireEvent } from "@testing-library/react"; import { LikeButton } from "./LikeButton"; describe("LikeButton", () => { it("点击切换点赞状态", () => { render(<LikeButton postId={1} />); const btn = screen.getByRole("button"); expect(btn).toHaveTextContent("点赞"); fireEvent.click(btn); expect(btn).toHaveTextContent("已赞"); }); });

2.3 端到端测试(Playwright)

模拟真实用户操作整个页面流程:

npm install -D @playwright/test npx playwright install chromium
// e2e/login.spec.ts import { test, expect } from "@playwright/test"; test("用户能登录并看到仪表盘", async ({ page }) => { await page.goto("/login"); await page.getByLabel("邮箱").fill("alice@example.com"); await page.getByLabel("密码").fill("secret123"); await page.getByRole("button", { name: "登录" }).click(); await expect(page).toHaveURL(/\/dashboard/); await expect(page.getByText("欢迎,alice")).toBeVisible(); });

工程实践要点:配置与策略

3.1 vitest 配置

// vitest.config.ts import { defineConfig } from "vitest/config"; import path from "path"; export default defineConfig({ test: { environment: "jsdom", setupFiles: ["./vitest.setup.ts"], }, resolve: { alias: { "@": path.resolve(__dirname) }, }, });

3.2 package.json 脚本

{ "scripts": { "test": "vitest", "test:unit": "vitest run", "test:e2e": "playwright test" } }

3.3 测试策略:测什么

层级 重点 频率
单元 工具函数、表单校验、数据格式化
组件 交互行为、状态切换、条件渲染
集成 页面 + 数据流(服务器组件)
端到端 登录、下单、核心流程

关键路径优先:登录注册、支付、权限、数据 CRUD——这些坏了代价最高。

3.4 CI 集成

# .github/workflows/test.yml steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npm ci - run: npm run lint - run: npm run test:unit - run: npm run test:e2e

CI 强制:测试不过不允许合并——让测试成为"质量闸门"。

常见误区与排查

误区 现象 正解
只测状态码/快照 逻辑坏了测不出 断言行为与内容
端到端写太多 慢、脆弱 只覆盖核心流程
测试互相依赖 单个过一起挂 每个测试独立
忘 mock 外部 API 测试连真实服务 用 vi.mock 隔离
测试不进 CI 形同虚设 CI 强制跑

动手演练:测试一个表单页面

// app/posts/new 的组件测试 // components/NewPostForm.test.tsx import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { NewPostForm } from "./NewPostForm"; // mock Server Action vi.mock("@/app/actions", () => ({ createPost: vi.fn(async () => ({ success: true })), })); describe("NewPostForm", () => { it("提交成功后调用 action", async () => { render(<NewPostForm />); fireEvent.change(screen.getByPlaceholderText("标题"), { target: { value: "我的文章" }, }); fireEvent.click(screen.getByRole("button", { name: "发布" })); await waitFor(() => { expect(createPost).toHaveBeenCalled(); }); }); it("校验失败显示错误", async () => { render(<NewPostForm />); fireEvent.click(screen.getByRole("button", { name: "发布" })); expect(await screen.findByText("标题必填")).toBeInTheDocument(); }); });

组件测试把表单交互与校验锁死——以后改表单逻辑,测试立刻告诉你哪里坏了。

一节小结

  • 三层测试:单元(Vitest)、组件(Testing Library)、端到端(Playwright)。
  • 测试价值:敢改代码——回归有保障。
  • 策略:关键路径优先,端到端少而精。
  • mock 隔离:外部 API/依赖用 vi.mock 隔离。
  • CI 闸门:测试不过不允许合并。
  • 断言行为:不只状态码/快照,断言内容与交互结果。

深入理解:测试策略的落地

测试金字塔

从上到下:数量递增、速度递增、成本递减。预算分配:单元与组件占大头,端到端只覆盖核心流程。

测试的三种隔离

隔离对象 做法
外部 API vi.mock 拦截 fetch
数据库 测试库或 mock Prisma
认证 mock auth() 返回固定用户
// 隔离示例:mock 认证与数据库 vi.mock("@/lib/auth", () => ({ auth: vi.fn(async () => ({ user: { id: "1" } })) })); vi.mock("@/lib/db", () => ({ prisma: { post: { create: vi.fn() } } }));

测试编写节奏

  1. 先写会挂的测试:新功能先写断言再实现(TDD 的基本节奏);
  2. 一个测试一件事:断言聚焦,失败时一眼定位;
  3. 断言行为而非实现:测"点击后显示已赞",不测"调用了某个函数";
  4. 跑得快要愿意跑:vitest watch 模式,改代码即时反馈。

一句话:测试的价值 = 敢于重构。测试网越密,改代码的胆子越大——但要把预算花在"容易坏且坏了代价高"的路径上(认证、数据写入、核心交互)。

常见问题速答

问:测试要覆盖到什么程度?
优先覆盖"容易坏且坏了代价高"的:认证、数据写入、核心交互、表单校验。纯展示组件可选。覆盖率不是目标,关键路径有保障才是

问:mock 和真实环境怎么平衡?
单元/组件测试用 mock(隔离外部依赖,快速稳定);端到端测试用真实环境(模拟真实用户)。两者互补:mock 测逻辑、e2e 测集成。

问:测试数据库怎么隔离?
方案:测试库(独立 SQLite/测试 Postgres)或 mock Prisma。测试前后清空数据,保证每个测试独立。绝不连生产数据库

问:vitest 和 jest 怎么选?
新项目用 Vitest(快、原生 ESM、配置少);存量 jest 项目可保留。两者对 Next.js 都有官方示例配置。

问:测试失败怎么排查?
先看断言信息(期望 vs 实际);再确认 mock 是否生效;最后检查测试数据是否独立。常见坑:异步未等待(用 waitFor/findBy)、mock 未清理(beforeEach clearAllMocks)。

一句话总结

测试是"改代码的勇气来源"——Vitest 测逻辑、Testing Library 测组件、Playwright 测流程。策略:关键路径优先、mock 隔离外部依赖、CI 强制闸门。测试的价值不在覆盖率数字,而在"敢于重构"的信心——把预算花在容易坏且坏了代价高的地方。

动手练习建议

为练习项目建立测试体系:配置 Vitest,给工具函数(如格式化、校验)写单元测试;给一个交互组件(登录表单)写组件测试(含 mock Server Action);再配置 Playwright 写一个端到端用例(注册 → 登录 → 创建文章 → 列表可见)。

把测试接入 CI(GitHub Actions:lint + test:unit + build),体验"提交被闸门挡住"的流程。再故意改坏一个函数(如密码校验少一行),确认测试变红——这就是测试给你的"敢改代码"的底气


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U