📚 全栈开发学习系列
从零到全栈 · 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:

运行 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 提供四个生命周期钩子:

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:

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 的核心区别:

运行:

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 的契约不被破坏。