Skip to content

测试

为什么需要测试?

项目规模小时,手动测试似乎够了。但随着功能增加,每次改动都可能引入新 Bug,手动回归测试成本越来越高。自动化测试让你改代码时有信心——跑一遍测试就知道有没有破坏已有功能。

为什么选 Vitest 而不是 Jest?

Vitest 原生支持 ESM(Nuxt 基于 ESM)、与 Vite 共享配置、启动速度快数倍、对 TypeScript 支持更好。Jest 需要复杂的 ESM 转换配置,在 Nuxt 项目中经常遇到兼容问题。

安装

bash
npm install -D vitest @vue/test-utils @nuxt/test-utils

TIP

三个包各有分工:vitest 是测试运行器 @vue/test-utils 提供组件挂载/交互 API,@nuxt/test-utils 提供 Nuxt 环境模拟(自动导入、插件等)

配置

ts
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
  // defineVitestConfig 自动加载 Nuxt 配置
  // 这意味着自动导入、插件、组件等在测试中都能正常工作
})

INFO

️ **必须使用 defineVitestConfig 而非 Vitest 原生的 defineConfig ** 原生配置不包含 Nuxt 的自动导入和插件系统,测试中 refuseFetch 等都会报 "is not defined" 错误

组件测试

方式一:mountSuspended(推荐)

为什么推荐 mountSuspended

它在完整的 Nuxt 环境中挂载组件,自动导入、插件、Provide/Inject 都能正常工作。而普通 mount 不包含 Nuxt 上下文,很多组合式函数会报错。

ts
// 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

ts
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')
  })
})

选择原则

如果组件中使用了 useRouteruseFetchuseState 等 Nuxt 组合式函数 → 用 mountSuspended。如果是纯展示组件,无 Nuxt 依赖 → 用 mount(更快)。

API 测试

ts
// 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 组合式函数

ts
// 测试依赖 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 请求

ts
// 使用 vitest 的 mock 功能拦截 $fetch
vi.mock('$fetch', () => ({
  default: vi.fn(() => Promise.resolve({ users: [] })),
}))

E2E 测试

使用 Playwright 进行端到端测试——模拟真实用户操作,验证完整的功能流程:

bash
npm install -D @playwright/test
ts
// 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 验证。

运行测试

bash
# 运行单元测试
npx vitest

# 监听模式(文件变化自动重跑)
npx vitest --watch

# 运行 E2E 测试
npx playwright test

# 查看覆盖率
npx vitest run --coverage

package.json 脚本

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/configdefineVitestConfig
useRouter is not defined普通 mount 不包含 Nuxt 上下文改用 mountSuspended
测试中 useState 返回 nullSSR 状态在测试环境中未初始化使用 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、简单赋值)会增加维护成本而没有实际价值。优先覆盖:核心业务逻辑、边界条件、错误处理路径。

知识脉络

text
测试
├── 单元测试(Vitest)
│   ├── 组件测试 → mountSuspended / mount
│   ├── API 测试  → setupTest
│   └── Mock 策略 → vi.mock
├── E2E 测试(Playwright)
├── 类型检查 → 详见《类型检查》
└── 调试技巧 → 详见《调试》

基于 Nuxt 4 官方文档整理编写