📚 全栈开发学习系列
从零到全栈 · 8 个阶段 · 24+ 篇
✅ 阶段一:编程基础
01-08
路线总览 · Python · C语言 · 数据结构 · Git · Linux
✅ 阶段二:Web开发基础
09-16
HTML · CSS · JavaScript · HTTP · FastAPI · PostgreSQL · 认证授权 · 综合实战
阶段三:前端深化
17-24 · 进行中
✓ 17 React入门:组件化思想与JSX语法
✓ 18 React进阶:状态管理与副作用钩子
✓ 19 TypeScript入门:类型系统与类型注解
✓ 20 Next.js基础:SSR与路由体系
✓ 21 Tailwind CSS入门:实用优先的CSS框架
✓ 22 前端工程化:Vite与Webpack构建工具
▸ 23 前端测试:Vitest与Playwright(当前)
○ 24 API测试:接口自动化与Mock服务
你写了一个函数,手动跑了一遍,结果正确,上线。第二天用户输入了一个你没料到的负数,程序崩了。
这不是运气问题,是测试缺位问题。本篇用 Vitest 4.1 做单元测试、Playwright 1.62 做端到端测试,从"手动验证"升级到"自动化回归"。读完本篇你将能为任意函数编写测试、模拟依赖模块、编写浏览器自动化脚本。
📋 目录
1. 为什么需要前端测试
2. Vitest 4.1:原生ESM的测试框架
3. 编写第一个单元测试
4. 测试钩子与生命周期
5. 断言匹配器详解
6. Mock:隔离测试的关键
7. Playwright 1.62:现代E2E测试
8. 编写第一个端到端测试
9. 页面交互与自动等待
10. Vitest vs Playwright对比与最佳实践
1. 为什么需要前端测试
前端测试像一个剧场彩排——剧本(代码)写完后,不直接面向观众(用户),而是先在舞台上完整走一遍。演员(函数)是否记住台词(返回正确值)、道具(数据)是否到位、场景切换(状态变更)是否流畅,都在彩排中暴露问题。
没有彩排就开演,等于把 bug 直接推到用户面前。没有测试就上线,同理。
前端测试分三层,构成测试金字塔:
| 层级 |
工具 |
测试对象 |
速度 |
占比 |
| 单元测试 |
Vitest |
单个函数/组件 |
毫秒级 |
~70% |
| 集成测试 |
Vitest |
多模块协作 |
秒级 |
~20% |
| 端到端测试 |
Playwright |
完整用户流程 |
秒~十秒 |
~10% |
金字塔底部最多——单元测试跑得快、写得多,覆盖每个函数的边界条件。顶部最少——E2E 测试慢但价值高,模拟真实用户从头到尾的操作路径。
💡 小贴士
不要一上来就写 E2E。先写单元测试覆盖核心逻辑,再补充 E2E 验证关键用户路径。反着来会导致测试又慢又脆——改一行代码,十个 E2E 全挂。
2. Vitest 4.1:原生ESM的测试框架
Vitest 是 Vite 团队推出的测试框架,与 Vite 共享配置和转换管线。Vitest 4.1(2026年3月发布)是目前最新稳定大版本,当前补丁版本为 4.1.11(2026年8月)。
相比 Jest 的核心优势:原生 ESM 支持、零配置对接 Vite 项目、启动时间从 Jest 的 3-5 秒降到 300ms 以内。
安装(在已有 Vite/Next.js 项目中):
bash
npm install -D vitest @vitest/ui
# 或使用 pnpm / yarn
pnpm add -D vitest @vitest/ui
安装后在 package.json 中添加测试脚本:
package.json
{
"scripts": {
"test": "vitest", // watch 模式
"test:run": "vitest run", // 单次运行
"test:ui": "vitest --ui" // 可视化界面
}
}
运行 npm test 即可启动 watch 模式——文件保存自动重新运行相关测试,无需手动触发。
3. 编写第一个单元测试
先写一个待测函数,再为它写测试。被测文件和测试文件放在同一目录,测试文件名用 .test.ts 或 .spec.ts 后缀。
被测函数:
src/utils/calculate.ts
export function add(a: number, b: number): number {
return a + b
}
export function formatPrice(cents: number): string {
return `¥${(cents / 100).toFixed(2)}`
}
测试文件:
src/utils/calculate.test.ts
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import { describe, it, expect } from 'vitest'
import { add, formatPrice } from './calculate'
describe('add', () => {
it('should add two positive numbers', () => {
expect(add(1, 2)).toBe(3)
})
it('should handle negative numbers', () => {
expect(add(-1, -2)).toBe(-3)
})
})
describe('formatPrice', () => {
it('should format cents to currency', () => {
expect(formatPrice(9950)).toBe('¥99.50')
})
})
|
三个核心 API:
- describe — 分组,把相关测试组织在一起,可嵌套
- it(别名
test)— 定义一个测试用例,第一个参数是描述文字
- expect — 断言,接收实际值,后接匹配器(如
toBe)验证预期
运行 npm test 后输出:
terminal output
✓ src/utils/calculate.test.ts (3)
✓ add (2)
✓ should add two positive numbers
✓ should handle negative numbers
✓ formatPrice (1)
✓ should format cents to currency
Test Files 1 passed (1)
Tests 3 passed (3)
4. 测试钩子与生命周期
测试经常需要准备数据和清理状态。Vitest 提供四个生命周期钩子:
- beforeAll — 所有测试开始前执行一次(如连接数据库)
- afterAll — 所有测试结束后执行一次(如关闭连接)
- beforeEach — 每个测试开始前执行(如重置数据)
- afterEach — 每个测试结束后执行(如清理临时文件)
src/utils/cart.test.ts
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import { beforeEach, afterEach, describe, it, expect } from 'vitest'
import { createCart, addItem } from './cart'
describe('shopping cart', () => {
let cart
beforeEach(() => {
cart = createCart() // 每个测试拿到全新购物车
})
afterEach(() => {
cart = null // 清理引用,避免内存泄漏
})
it('starts empty', () => {
expect(cart.items).toHaveLength(0)
})
})
|
beforeEach 保证每个 it 拿到的购物车是干净的——第一个测试加了商品不会影响第二个测试的结果。这叫测试隔离。
5. 断言匹配器详解
expect() 返回一个断言对象,后接匹配器方法。常用匹配器:
| 匹配器 |
用途 |
示例 |
| toBe |
原始值严格相等(===) |
expect(1+1).toBe(2) |
| toEqual |
对象/数组深度相等 |
expect({a:1}).toEqual({a:1}) |
| toContain |
数组/字符串包含元素 |
expect([1,2]).toContain(2) |
| toMatch |
正则匹配字符串 |
expect('hi').toMatch(/^h/) |
| toHaveLength |
验证长度 |
expect('abc').toHaveLength(3) |
| toThrow |
函数抛出异常 |
expect(() => fn()).toThrow() |
| toBeTruthy / Falsy |
真值/假值断言 |
expect(1).toBeTruthy() |
| toBeNull / Undefined |
null / undefined 断言 |
expect(null).toBeNull() |
| toHaveBeenCalled |
Mock 函数被调用过 |
expect(mockFn).toHaveBeenCalled() |
所有匹配器都可用 .not 取反:
TypeScript
// 验证函数不会返回 NaN
expect(add(1, 2)).not.toBeNaN()
// 验证数组不包含某个元素
expect([1, 2, 3]).not.toContain(4)
6. Mock:隔离测试的关键
被测函数往往依赖外部模块——API 请求、数据库、文件系统。在单元测试中直接调用真实依赖会导致测试又慢又不稳定。Mock 用假实现替换真依赖,让测试只关注被测函数自身的逻辑。
Vitest 提供两种 Mock:
- vi.fn() — 创建模拟函数,可控制返回值和验证调用
- vi.mock() — 替换整个模块,在 import 前生效
src/utils/user.test.ts
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import { vi, describe, it, expect } from 'vitest'
// 替换 ./api 模块,fetchUser 返回固定数据
vi.mock('./api', () => ({
fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: 'Alice' }))
}))
import { fetchUser } from './api'
import { displayUser } from './user'
it('displays user name', async () => {
const result = await displayUser(1)
expect(result).toBe('User: Alice')
})
|
vi.mock 在文件顶部被提升执行(hoisting),先于所有 import。这样 import { fetchUser } 拿到的是 Mock 版本而非真实 API。测试不会发出网络请求,速度稳定在毫秒级。
还可以验证 Mock 函数的调用参数:
TypeScript
// 验证 fetchUser 被用参数 1 调用了一次
expect(fetchUser).toHaveBeenCalledWith(1)
// 验证恰好调用一次
expect(fetchUser).toHaveBeenCalledTimes(1)
⚠️ 常见错误:vi.mock 的变量引用
错误写法:在 vi.mock 工厂函数内引用外部变量会报 ReferenceError: Cannot access before initialization。
原因:vi.mock 被提升到文件顶部执行,此时外部变量还未初始化。解决方法:把固定数据直接写在工厂函数内部,或使用 vi.hoisted() 包裹。
7. Playwright 1.62:现代E2E测试
单元测试验证函数逻辑正确。但用户不关心函数——用户关心"打开页面→输入→点击→看到结果"这个完整流程是否正常。Playwright 1.62(微软出品)用真实浏览器执行 E2E 测试,当前最新补丁版本为 1.62.1。
Playwright 1.62 新特性:WebP 格式截图、新的组件测试模型、改进的 accessibility snapshot。
安装:
bash
# 安装 Playwright 测试框架
npm install -D @playwright/test
# 安装浏览器二进制(chromium/firefox/webkit)
npx playwright install chromium
安装后创建配置文件:
playwright.config.ts
|
1
2
3
4
5
6
7
8
9
|
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './e2e', // 测试文件目录
use: {
baseURL: 'http://localhost:3000',
headless: true // 无头模式,CI 友好
}
})
|
💡 小贴士
首次安装浏览器会下载约 150MB,之后缓存在系统目录。换项目时无需重复下载。若下载失败,设置 PLAYWRIGHT_DOWNLOAD_HOST 环境变量切换镜像源。
8. 编写第一个端到端测试
确保本地 dev server 已启动(npm run dev),然后编写测试:
e2e/home.spec.ts
|
1
2
3
4
5
6
7
8
9
10
|
import { test, expect } from '@playwright/test'
test('homepage displays welcome heading', async ({ page }) => {
await page.goto('/')
const heading = page.locator('h1')
await expect(heading).toHaveText('Welcome')
await expect(page).toHaveTitle(/My App/)
})
|
与 Vitest 的核心区别:
test 代替 it,来自 @playwright/test 而非 vitest
- 每个
test 接收 { page } fixture——一个真实浏览器页面实例
expect 前必须加 await——浏览器操作是异步的
运行:
bash
# 运行所有 E2E 测试
npx playwright test
# 以有头模式运行(看到浏览器窗口)
npx playwright test --headed
# 生成 HTML 测试报告
npx playwright show-report
9. 页面交互与自动等待
Playwright 最强的能力是自动等待——所有操作(点击、填充、读取)在元素准备好之前会自动重试,无需手动写 setTimeout 或 waitFor。
一个完整的登录流程测试:
e2e/login.spec.ts
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import { test, expect } from '@playwright/test'
test('login flow', async ({ page }) => {
await page.goto('/login')
// 填充表单——自动等待输入框出现
await page.fill('[data-testid="email"]', 'alice@test.com')
await page.fill('[data-testid="password"]', 'secret123')
await page.click('[data-testid="submit"]')
// 验证跳转到仪表盘
await expect(page.locator('.dashboard')).toBeVisible()
await expect(page.locator('.welcome-msg')).toContainText('Alice')
})
|
常用页面操作:
| 方法 |
用途 |
自动等待 |
| page.goto(url) |
导航到 URL |
等待 load 事件 |
| page.fill(sel, val) |
填充输入框 |
等待元素可交互 |
| page.click(sel) |
点击元素 |
等待元素可见可点 |
| page.locator(sel) |
定位元素 |
链式断言时等待 |
| expect(loc).toBeVisible() |
验证元素可见 |
自动重试直到超时 |
⚠️ 常见错误:忘记 await
错误写法:expect(page.locator('.msg')).toHaveText('ok')(缺少 await)
原因:Playwright 的 expect 返回 Promise,不加 await 会导致断言尚未完成测试就结束了。Vitest 的 expect 是同步的,所以从 Vitest 迁移过来时最容易踩这个坑。始终写 await expect(...)。
10. Vitest vs Playwright 对比与最佳实践
| 维度 |
Vitest |
Playwright |
| 定位 |
单元/集成测试 |
端到端测试 |
| 运行环境 |
Node.js |
真实浏览器 |
| 速度 |
毫秒级 |
秒级 |
| 测试对象 |
函数、模块、组件 |
完整用户流程 |
| Mock 能力 |
vi.fn / vi.mock |
page.route 拦截 |
| 依赖 |
无需浏览器 |
需安装浏览器 |
| CI 适用 |
直接运行 |
需 headless 模式 |
最佳实践:两者配合使用,不是二选一。用 Vitest 写 70% 的单元测试覆盖核心逻辑,用 Playwright 写 10% 的 E2E 测试覆盖关键用户路径(登录、下单、搜索)。中间 20% 用 Vitest 的集成测试模式覆盖多模块协作。
分级练习
基础(理解概念)
1. 为以下函数编写 3 个单元测试,覆盖正常值、边界值和异常值:
function clamp(n: number, min: number, max: number): number
2. 用
describe 分组组织上述测试,每组包含
beforeEach 钩子。
进阶(实际应用)
3. 创建一个 userApi.ts 模块(含 fetchUser 和 updateUser),用 vi.mock 模拟 API 返回值并验证调用参数。
4. 编写 Playwright E2E 测试:访问本地 Next.js 项目首页,验证导航栏存在且点击链接能正确跳转。
挑战(综合实战)
5. 搭建完整的测试体系:为一个包含表单提交的页面同时编写 Vitest 单元测试(验证提交逻辑)和 Playwright E2E 测试(验证从填写到提交到看到结果页的完整流程)。在 package.json 中配置 test:unit 和 test:e2e 两个脚本,用 npm test 一键运行全部。
📌 知识回顾
测试金字塔
Vitest 4.1
describe / it / expect
生命周期钩子
匹配器
vi.fn / vi.mock
Playwright 1.62
自动等待
page.locator
测试隔离
下一篇预告
24 API 测试:接口自动化与 Mock 服务
前端测试覆盖了 UI 和用户流程,但 API 本身的正确性如何验证?下一篇用 Vitest 的 supertest 集成和 Mock Service Worker(MSW)搭建接口测试体系,从请求构造到响应断言,确保后端 API 的契约不被破坏。