主题
19 · 测试策略:单元 / 组件 / E2E
目标:说清「测什么、不测什么」;能选单元 / 组件 / E2E 的边界;Mock 不越界;写出 2~3 个有效测试草图。
定位:工程闭环,不是测试框架 API 大全。
背景
很多团队要么「零测试」,要么「全 E2E 跑一夜」。面试也常问:为什么不用 snapshot 包打天下?
Journal 仓库里已有 client / admin / backend 三套 Vitest(或同类)单测,client 还有 Playwright 关键路径 E2E——本篇讲策略,不逐文件导读。
概念:三层测试与职责
| 层级 | 测什么 | 典型工具 | 速度 | 稳定性 |
|---|---|---|---|---|
| 单元 | 纯函数、格式化、校验、store 纯逻辑 | Vitest / Jest | 毫秒级 | 高 |
| 组件 | 渲染、交互、props / emit、可访问性底线 | Vitest + Testing Library | 秒级 | 中 |
| E2E | 跨页面用户路径、真实路由与网络 | Playwright / Cypress | 分钟级 | 低(环境敏感) |
测试金字塔直觉:单元多、组件适中、E2E 少而精——E2E 覆盖「钱在烧」的关键路径即可。
测什么 / 不测什么
优先测:
- 有分支的业务规则(权限、计费、日期边界、空态)
- 容易回归的纯函数(
formatDate、parseApiError) - 用户无法容忍坏掉的路径(登录、下单、发布——Journal 对应:登录后写笔记、列表加载)
可以少测或不测:
- 第三方 UI 库内部行为
- 一行 getter、无分支的 re-export
- 纯样式(除非 a11y 对比度等可断言项)
- 「抄官方 demo 且零定制」的胶水代码
实践:Mock 边界与示例草图
Mock 边界(默认原则)
| 测层级 | Mock 什么 | 尽量不 Mock |
|---|---|---|
| 单元 | 时间、Math.random、外部 I/O | 被测函数本身 |
| 组件 | HTTP 层 / store 注入 | DOM 浏览器 API(用 jsdom 即可) |
| E2E | 仅第三方不可控(支付沙箱) | 自己后端——尽量起真实 API 或 contract 环境 |
反模式:单元测试里 Mock 了 half 个应用,断言却还在验「集成对不对」——那是 E2E 的事。
草图 1 · 单元(Vitest)
js
// utils/format-note-preview.test.js
import { describe, it, expect } from 'vitest'
import { previewText } from './format-note-preview'
describe('previewText', () => {
it('空字符串返回占位', () => {
expect(previewText('')).toBe('暂无内容')
})
it('超长截断并加省略', () => {
expect(previewText('a'.repeat(100), 20)).toMatch(/…$/)
})
})1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
草图 2 · 组件(Testing Library)
js
import { render, screen, fireEvent } from '@testing-library/vue'
import SortToolbar from '@/components/sort-toolbar.vue'
it('切换排序 emit 正确值', async () => {
const onSort = vi.fn()
render(SortToolbar, { props: { modelValue: 'time' }, attrs: { onSort } })
await fireEvent.click(screen.getByRole('button', { name: '按标题' }))
expect(onSort).toHaveBeenCalledWith('title')
})1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
草图 3 · E2E(Playwright)
js
// e2e/p0-login-and-list.spec.js
import { test, expect } from '@playwright/test'
test('登录后能看到笔记列表', async ({ page }) => {
await page.goto('/')
// H5 dev:粘贴 JWT 或走测试账号 seed
await expect(page.getByTestId('note-list')).toBeVisible({ timeout: 15_000 })
})1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Journal 根目录 pnpm test 并行跑 client / admin / backend;E2E 需 backend + seed,见 AGENTS.md 与 docs/spec/qa/。
踩坑
- 测实现细节:断言内部
ref名或私有方法——重构必炸。改断言用户可见结果或 emit。 - E2E 过多:CI 30 分钟没人敢跑 → 形同虚设。按 P0 / P1 分层。
- Mock fetch 却不断言 URL / body:假绿。至少验 method、path、关键字段。
- 没有失败用例:只测 happy path,边界全留给线上。
- 覆盖率崇拜:60% 行覆盖但关键路径零 E2E,不如反过来。
验收
- [ ] 能口述三层测试各适合什么、不适合什么
- [ ] 给一个 util 写 3 条 table-driven 用例(含边界)
- [ ] 给一个交互组件写 1 个「点击 → emit / 文案变化」测试
- [ ] 列出项目 3 条 P0 E2E(不超过 10 条总数)
- [ ] 说清 Mock 停在哪一层
下一步
参考
- Vitest
- Vue Testing Library
- Playwright
- Journal:
docs/spec/qa/client-单元测试规划.md、docs/spec/qa/手帐小程序测试路径.md - 仓库根
pnpm test/pnpm e2e:client
