前端测试金字塔:单元、组件、E2E
前端测试分三层,像金字塔:底层是单元测试(最多、最快、测函数和工具)、中层是组件测试(测单个组件的渲染和交互)、顶层是 E2E 测试(最少、最慢、模拟真实用户操作整个流程)。
为什么叫金字塔?因为越往下应该越多:单元测试应该占 70%,组件测试占 20%,E2E 只占 10%。E2E 写起来慢、跑起来慢、还容易因为界面变化而挂掉——所以它只覆盖最关键的业务流程(登录、下单、支付),不覆盖所有细节。
不要一上来就写 E2E。先用单元测试把工具函数和业务逻辑覆盖住,再用组件测试测交互,最后挑 3-5 条核心路径写 E2E。
- VitestVite 原生测试框架,零配置、极速、API 兼容 Jest。
- Testing Library渲染组件,模拟用户交互,断言 DOM 状态。
- Playwright浏览器自动化,写 E2E 测试像写操作脚本。
- Mock拦截网络请求和模块依赖,让测试只测当前单元。
Vitest:前端单元测试的新标准
Jest 曾经是前端测试的唯一选择,但 Vitest 出来后情况变了。Vitest 和 Vite 共用同一套配置,启动速度极快,API 和 Jest 几乎一样——迁移成本很低。现在新项目用 Vitest 是主流。
npm install -D vitest
# 写一个测试文件 sum.test.js
import { add } from "./sum"
import { expect, test } from "vitest"
test("1 + 2 = 3", () => {
expect(add(1, 2)).toBe(3)
})
test("负数相加", () => {
expect(add(-1, -2)).toBe(-3)
})
运行 npx vitest,它会自动找到所有 *.test.js 文件并执行。和 Jest 一样,expect 提供各种断言:toBe、toEqual、toContain、toBeGreaterThan。
| 断言 | 用途 |
|---|---|
| toBe(x) | 严格相等(===) |
| toEqual(obj) | 深度比较对象 |
| toContain(x) | 数组/字符串包含 |
| toThrow() | 函数抛异常 |
| toBeTruthy() | 值为真 |
Vitest 的 watch 模式很好用:改代码后自动重跑相关测试,开发时实时看到测试状态。
组件测试:Testing Library
组件测试比单元测试高一档:你把一个 React/Vue 组件渲染到虚拟 DOM 里,模拟用户点击、输入,然后断言页面显示的内容对不对。
import { render, screen, fireEvent } from "@testing-library/react"
import Counter from "./Counter"
test("点击按钮数字加一", () => {
render(<Counter />)
// 找到按钮,模拟点击
const btn = screen.getByText("+1")
fireEvent.click(btn)
// 断言显示的数字变了
expect(screen.getByText("1")).toBeInTheDocument()
})
Testing Library 的理念是像用户一样找元素——通过文本、角色、标签找,而不是通过 CSS 类名。这样测试和界面的视觉设计解耦:你改按钮颜色、调间距,测试不会挂;你改功能行为,测试会挂。
常用查询方式:getByText(按文字找)、getByRole(按 ARIA 角色找)、getByLabelText(按表单标签找)。这些都是用户视角的定位方式。
Playwright:端到端测试实战
E2E 测试是真正打开浏览器、访问你的网站、模拟用户操作。Playwright 是目前最好的 E2E 工具——它支持 Chromium、Firefox、WebKit 三个浏览器,自动等待、截图、录屏都内置。
npm init playwright@latest
// tests/login.spec.ts
import { test, expect } from "@playwright/test";
test("用户可以登录", async ({ page }) => {
await page.goto("https://localhost:5173/login");
// 填写表单
await page.getByLabel("邮箱").fill("test@example.com");
await page.getByLabel("密码").fill("123456");
// 点击登录
await page.getByRole("button", { name: "登录" }).click();
// 断言跳转到首页
await expect(page).toHaveURL(/\/home/);
await expect(page.getByText("欢迎回来")).toBeVisible();
});
运行 npx playwright test,它会打开浏览器,自动走完整个流程。任何一步失败,它自动截图、保留日志、甚至录视频——调试极其方便。
Playwright 的超能力:自动等待。你写 click(),它会等元素出现、可点击、不被遮挡才点——不需要手动写 waitFor。这比老工具 Selenium 省心太多。
Mock 网络请求与模块
组件和单元测试不应该真的发 HTTP 请求——那样测试会慢、不稳定、还依赖后端可用。Vitest 提供了 mock 机制:把 fetch/axios 替换成假的,预设返回值。
import { vi, expect, test } from "vitest"
// mock 掉 fetch
vi.stubGlobal("fetch", vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: "测试用户" })
})
))
test("加载用户信息", async () => {
const user = await fetchUser()
expect(user.name).toBe("测试用户")
expect(fetch).toHaveBeenCalledWith("/api/user")
})
这样测试只验证"我们的代码是否正确调用了接口、正确处理了返回值",不依赖真实后端。测试跑得快、结果稳定。
Playwright 也支持拦截网络请求——在 E2E 测试里 mock 后端接口,让前端测试不依赖后端服务是否可用。page.route("**/api/**", handler) 可以拦截所有 API 请求返回假数据。
CI 集成与测试报告
测试不接 CI 等于白写。GitHub Actions 里加一步,每次提交自动跑测试:
# .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx vitest run
- run: npx playwright test
这样任何人提交代码,CI 自动跑单元测试和 E2E。测试不通过就拦住合并——主分支永远保持可工作状态。
测试报告方面:Vitest 配合 --coverage 输出覆盖率报告;Playwright 自动生成 HTML 报告,失败时附带截图和视频。CI 里把报告作为 artifact 上传,失败时下载来看哪一步挂了。
写多少测试才够:务实策略
不是所有代码都要写测试。务实的优先级是:
- 纯函数和工具库:100% 覆盖——它们输入输出明确,测试好写、价值高。
- 业务逻辑核心:高覆盖——计算规则、状态流转、权限判断,错了就是事故。
- UI 组件:只测关键交互——按钮点击、表单提交、错误提示。样式不写测试。
- 第三方集成:写一条 E2E 冒烟就行——确认能调通,不写细枝末节。
- 胶水代码:不写测试——就是组装调用,写错了一眼能看出来。
测试是为了让你改代码时有底气,不是为了追求覆盖率数字。写测试的时间应该花在"改了会出大事"的地方,而不是均匀铺在所有代码上。