本节摘要:应用越大越不敢改——测试就是"改代码的勇气来源"。本节讲清 Next.js 测试体系:单元测试(Vitest)、组件测试(Testing Library)、端到端测试(Playwright),以及"测试什么、怎么组织"的策略。
阅读完本节,你应当能够:
功能能跑 ≠ 能维护。没有测试的代码库:改 A 功能,B 悄悄坏了,没人发现。测试的价值不是"证明现在对",而是"以后敢改"——有了测试网,重构、升级依赖、加功能都不怕。
直觉类比:测试像"安全网"——走钢丝(改代码)的人,有网(测试)才敢做动作。没有网,只能原地不动(不敢改),或摔下去(上线出 bug)。
💡 关键直觉:测试分层投入——纯逻辑用单元测试(最多、最快),组件/页面用组件测试,核心用户流程用端到端测试(最少、最贵)。把预算花在"容易坏且坏了代价高"的地方。
测纯函数与逻辑(工具函数、数据格式化、字典处理):
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"); }); });
测组件渲染与交互:
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("已赞"); }); });
模拟真实用户操作整个页面流程:
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(); });
// 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) }, }, });
{ "scripts": { "test": "vitest", "test:unit": "vitest run", "test:e2e": "playwright test" } }
| 层级 | 重点 | 频率 |
|---|---|---|
| 单元 | 工具函数、表单校验、数据格式化 | 多 |
| 组件 | 交互行为、状态切换、条件渲染 | 中 |
| 集成 | 页面 + 数据流(服务器组件) | 中 |
| 端到端 | 登录、下单、核心流程 | 少 |
关键路径优先:登录注册、支付、权限、数据 CRUD——这些坏了代价最高。
# .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(); }); });
组件测试把表单交互与校验锁死——以后改表单逻辑,测试立刻告诉你哪里坏了。
从上到下:数量递增、速度递增、成本递减。预算分配:单元与组件占大头,端到端只覆盖核心流程。
| 隔离对象 | 做法 |
|---|---|
| 外部 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() } } }));
一句话:测试的价值 = 敢于重构。测试网越密,改代码的胆子越大——但要把预算花在"容易坏且坏了代价高"的路径上(认证、数据写入、核心交互)。
问:测试要覆盖到什么程度?
优先覆盖"容易坏且坏了代价高"的:认证、数据写入、核心交互、表单校验。纯展示组件可选。覆盖率不是目标,关键路径有保障才是。
问: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),体验"提交被闸门挡住"的流程。再故意改坏一个函数(如密码校验少一行),确认测试变红——这就是测试给你的"敢改代码"的底气。