前端测试指南
这页适合作为“前端项目测试策略总览”。重点不是把测试写得越多越好,而是把单元测试、组件测试、E2E 测试分别放在最有价值的位置,避免测试维护成本反过来拖慢迭代。
推荐建立顺序
比较稳妥的测试落地方式通常是:
- 先补工具函数与关键业务逻辑的单元测试
- 再为高复用组件补组件测试
- 最后只给关键用户路径加少量 E2E 测试
这样做能更快见效,也更不容易因为 E2E 过多导致 CI 变慢、失败难排查。
Vitest
Vitest 是 Vite 生态的测试框架,兼容 Jest API。
安装
pnpm add -D vitest
基础用法
// sum.ts
export function sum(a: number, b: number) {
return a + b;
}
// sum.test.ts
import { describe, it, expect } from "vitest";
import { sum } from "./sum";
describe("sum", () => {
it("adds two numbers", () => {
expect(sum(1, 2)).toBe(3);
});
it("handles negative numbers", () => {
expect(sum(-1, 1)).toBe(0);
});
});
vitest --run # 单次运行
vitest --coverage # 覆盖率
常用断言
expect(value).toBe(42); // 严格相等
expect(value).toEqual({ a: 1 }); // 深度相等
expect(value).toBeTruthy(); // 真值
expect(value).toBeNull(); // null
expect(value).toContain("hello"); // 包含
expect(value).toHaveLength(3); // 长度
expect(fn).toThrow("error"); // 抛出错误
expect(fn).toHaveBeenCalledWith("arg"); // 调用参数
Mock
import { vi } from "vitest";
// 函数 mock
const fn = vi.fn();
fn("hello");
expect(fn).toHaveBeenCalledWith("hello");
// 模块 mock
vi.mock("./api", () => ({
fetchUser: vi.fn().mockResolvedValue({ name: "Domi" }),
}));
// 定时器 mock
vi.useFakeTimers();
setTimeout(() => {}, 1000);
vi.advanceTimersByTime(1000);
vi.useRealTimers();
Vue 组件测试
pnpm add -D @vue/test-utils happy-dom
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "happy-dom",
},
});
import { mount } from "@vue/test-utils";
import MyComponent from "./MyComponent.vue";
it("renders message", () => {
const wrapper = mount(MyComponent, {
props: { msg: "Hello" },
});
expect(wrapper.text()).toContain("Hello");
});
it("emits event on click", async () => {
const wrapper = mount(MyComponent);
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("submit")).toBeTruthy();
});
Playwright
Playwright 用于端到端(E2E)测试。
安装
pnpm add -D @playwright/test
npx playwright install
基础测试
// tests/home.spec.ts
import { test, expect } from "@playwright/test";
test("homepage loads", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/DomiVault/);
await expect(page.locator("h1")).toBeVisible();
});
test("search works", async ({ page }) => {
await page.goto("/");
await page.keyboard.press("Control+k");
await page.fill('input[placeholder*="搜索"]', "docker");
await expect(page.locator(".search-result-item")).toHaveCount(1);
});
test("navigation", async ({ page }) => {
await page.goto("/");
await page.click("text=文章");
await expect(page).toHaveURL("/docs");
});
配置
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
webServer: {
command: "pnpm dev",
port: 3000,
reuseExistingServer: true,
},
use: {
baseURL: "http://localhost:3000",
screenshot: "only-on-failure",
},
});
测什么最划算
可以用下面的思路判断:
- 纯函数 / 数据转换 / 工具方法:优先 Vitest
- 组件渲染 / props / emits / 交互:优先组件测试
- 登录、搜索、下单、提交表单这类核心路径:优先 Playwright
相反,下面这些不一定值得重度测试:
- 纯静态展示且几乎不变的简单组件
- 依赖实现细节、改样式就会碎掉的脆弱选择器测试
- 可以被更小粒度测试覆盖的重复 E2E 流程
测试策略
单元测试(Vitest) → 工具函数、composables、纯逻辑
组件测试(Vue Test Utils) → 组件渲染、交互、事件
E2E 测试(Playwright) → 用户流程、页面导航、集成
经典测试金字塔:大量单元测试 → 适量集成测试 → 少量 E2E 测试。
常见问题
E2E 很不稳定
高频原因通常包括:
- 依赖脆弱的文案或 CSS 选择器
- 没等待页面真正稳定就断言
- 本地、CI、预览环境的数据和网络条件不一致
优先使用稳定的定位方式,例如 role、label、test id,并减少对动画、时间和随机数据的依赖。
Mock 太多,测了等于没测
单元测试里可以 mock,但不要把关键流程全部 mock 掉。否则测试能过,并不代表真实集成链路能过。
覆盖率高,不代表质量高
覆盖率只能说明代码被跑到了,不说明断言真的有价值。比起追 100% 覆盖率,更重要的是覆盖高风险路径、边界条件和错误处理。
CI 建议
- 轻量单元测试可以在每次 push / PR 都跑
- E2E 可以放在 PR、主分支或预览环境上跑
- 失败时保留截图、trace、视频,有助于快速回溯
延伸阅读
参考链接
- Vitest — 文档
- Playwright — 文档
- Vue Test Utils — Vue 组件测试
- Testing Library — 用户视角测试