测试
为什么需要测试?
项目规模小时,手动测试似乎够了。但随着功能增加,每次改动都可能引入新 Bug,手动回归测试成本越来越高。自动化测试让你改代码时有信心——跑一遍测试就知道有没有破坏已有功能。
为什么选 Vitest 而不是 Jest?
Vitest 原生支持 ESM(Nuxt 基于 ESM)、与 Vite 共享配置、启动速度快数倍、对 TypeScript 支持更好。Jest 需要复杂的 ESM 转换配置,在 Nuxt 项目中经常遇到兼容问题。
安装
npm install -D vitest @vue/test-utils @nuxt/test-utilsTIP
三个包各有分工:vitest 是测试运行器 @vue/test-utils 提供组件挂载/交互 API,@nuxt/test-utils 提供 Nuxt 环境模拟(自动导入、插件等)
配置
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
// defineVitestConfig 自动加载 Nuxt 配置
// 这意味着自动导入、插件、组件等在测试中都能正常工作
})INFO
️ **必须使用 defineVitestConfig 而非 Vitest 原生的 defineConfig ** 原生配置不包含 Nuxt 的自动导入和插件系统,测试中 ref、useFetch 等都会报 "is not defined" 错误
组件测试
方式一:mountSuspended(推荐)
为什么推荐 mountSuspended?
它在完整的 Nuxt 环境中挂载组件,自动导入、插件、Provide/Inject 都能正常工作。而普通 mount 不包含 Nuxt 上下文,很多组合式函数会报错。
// tests/components/Button.spec.ts
import { mountSuspended } from '@nuxt/test-utils/runtime'
import Button from '~/app/components/Button.vue'
describe('Button', () => {
it('renders text', async () => {
const wrapper = await mountSuspended(Button, {
props: { label: 'Click me' },
})
expect(wrapper.text()).toContain('Click me')
})
it('emits click event', async () => {
const wrapper = await mountSuspended(Button)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
})INFO
️ mountSuspended 是异步的 必须用 await。普通 mount 是同步的,但无法访问 Nuxt 上下文
方式二:普通 mount(简单组件)
对于不依赖 Nuxt 组合式函数的纯 UI 组件,可以使用普通 mount:
import { mount } from '@vue/test-utils'
import Button from '~/app/components/Button.vue'
describe('Button', () => {
it('renders text', () => {
const wrapper = mount(Button, {
props: { label: 'Click me' },
})
expect(wrapper.text()).toContain('Click me')
})
})选择原则
如果组件中使用了 useRouter、useFetch、useState 等 Nuxt 组合式函数 → 用 mountSuspended。如果是纯展示组件,无 Nuxt 依赖 → 用 mount(更快)。
API 测试
// tests/api/users.spec.ts
import { setupTest } from '@nuxt/test-utils'
describe('Users API', () => {
const ctx = setupTest()
it('returns users list', async () => {
const { data } = await ctx.$fetch('/api/users')
expect(data).toBeInstanceOf(Array)
})
it('creates user', async () => {
const user = await ctx.$fetch('/api/users', {
method: 'POST',
body: { name: 'Alice', email: 'alice@example.com' },
})
expect(user.name).toBe('Alice')
})
})TIP
setupTest() 会启动一个真实的 Nuxt 服务器实例 ctx.$fetch 发送真实的 HTTP 请求。这比 mock 更接近生产环境,但运行速度较慢,适合 API 和集成测试
Mock 策略
为什么需要 Mock?
测试应该隔离外部依赖,否则网络请求失败、数据库变化都会导致测试不稳定。Mock 让你控制依赖的返回值,只测自己的逻辑。
Mock 组合式函数
// 测试依赖 useFetch 的组件
const mockData = ref({ name: 'Test User' })
// 方法一:在 mountSuspended 中通过 stub 传入
const wrapper = await mountSuspended(UserProfile, {
global: {
stubs: {
UserProfile: true,
},
},
})
// 方法二:使用 vi.mock 替换整个模块
vi.mock('#app', async () => {
const actual = await vi.importActual('#app')
return {
...actual,
useFetch: () => ({ data: mockData, pending: ref(false), error: ref(null) }),
}
})Mock API 请求
// 使用 vitest 的 mock 功能拦截 $fetch
vi.mock('$fetch', () => ({
default: vi.fn(() => Promise.resolve({ users: [] })),
}))E2E 测试
使用 Playwright 进行端到端测试——模拟真实用户操作,验证完整的功能流程:
npm install -D @playwright/test// e2e/home.spec.ts
import { test, expect } from '@playwright/test'
test('homepage has title', async ({ page }) => {
await page.goto('/')
await expect(page.locator('h1')).toContainText('Welcome')
})单元测试 vs E2E 测试的选择
单元测试验证单个函数/组件,速度快、定位问题精准;E2E 测试验证完整用户流程,更接近真实体验但速度慢、定位问题困难。建议:核心逻辑用单元测试覆盖,关键用户路径用 E2E 验证。
运行测试
# 运行单元测试
npx vitest
# 监听模式(文件变化自动重跑)
npx vitest --watch
# 运行 E2E 测试
npx playwright test
# 查看覆盖率
npx vitest run --coveragepackage.json 脚本
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:coverage": "vitest run --coverage"
}
}常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
ref is not defined | 未使用 defineVitestConfig 或未在组件中自动导入 | 确保使用 @nuxt/test-utils/config 的 defineVitestConfig |
useRouter is not defined | 普通 mount 不包含 Nuxt 上下文 | 改用 mountSuspended |
测试中 useState 返回 null | SSR 状态在测试环境中未初始化 | 使用 mountSuspended 或手动 provide Nuxt 上下文 |
vi.mock 不生效 | mock 语句在 import 之后 | vi.mock 会被提升到文件顶部,确保路径正确 |
| 组件测试报 "window is not defined" | 在 Node.js 环境中访问浏览器 API | 使用 mountSuspended 或 mock window 对象 |
| API 测试超时 | setupTest 启动 Nuxt 服务器需要时间 | 增加 test.timeout 或使用 beforeAll 预热 |
INFO
️ 测试文件位置: 单元测试建议放在 tests/ 目录下(与源码分离) 或使用 __tests__/ 目录与源码同目录。E2E 测试放在 e2e/ 或 tests/e2e/ 下
测试覆盖率建议
| 指标 | 建议目标 | 说明 |
|---|---|---|
| 语句覆盖率 | ≥ 80% | 核心业务逻辑应更高 |
| 分支覆盖率 | ≥ 70% | if/else 分支都要覆盖 |
| 函数覆盖率 | ≥ 80% | 每个导出函数至少一个测试 |
| 行覆盖率 | ≥ 80% | 与语句覆盖率类似 |
不要追求 100% 覆盖率
过度测试(如测试 getter、简单赋值)会增加维护成本而没有实际价值。优先覆盖:核心业务逻辑、边界条件、错误处理路径。
知识脉络
测试
├── 单元测试(Vitest)
│ ├── 组件测试 → mountSuspended / mount
│ ├── API 测试 → setupTest
│ └── Mock 策略 → vi.mock
├── E2E 测试(Playwright)
├── 类型检查 → 详见《类型检查》
└── 调试技巧 → 详见《调试》