← 返回文章列表
前端工程化·307 次阅读

Vitest 上手:给 Vue 3 项目补上单元测试

上周重构一个核心组件,改完手动回归花了两小时,心里默念了八遍"要是有测试就好了"。这次痛下决心把单元测试补上,选型没什么悬念——项目用 Vite,测试自然是 Vitest。

为什么不是 Jest

Jest 统治了很多年,但在 Vite 项目里它有两个别扭:

  1. 项目用 ESM + Vite 的转换管线,Jest 要用自己的一套(babel/ts-jest),同一套代码两套转换逻辑,配置双倍、行为可能不一致
  2. Jest 对原生 ESM 的支持一直磕磕绊绊

Vitest 直接复用 Vite 的转换和配置,API 还兼容 Jest——迁移成本几乎为零。

十分钟接入

npm i -D vitest @vue/test-utils jsdom

vite.config.ts 里加 test 配置:

/// <reference types="vitest" />
import { defineConfig } from 'vite'

export default defineConfig({
  // ...原有配置原封不动,Vitest 直接复用
  test: {
    environment: 'jsdom', // 组件测试需要 DOM 环境
    globals: true,        // 不用 import 就能用 describe/it/expect
  },
})
{
  "scripts": {
    "test": "vitest",           // watch 模式,改代码自动重跑
    "test:run": "vitest run",   // CI 用,跑完退出
    "coverage": "vitest run --coverage"
  }
}

启动速度是真快——冷启动一秒内,这就是原生 ESM + esbuild 的威力。

第一个测试:纯函数

从最简单的开始,测一个工具函数:

// utils/format.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice } from './format'

describe('formatPrice', () => {
  it('分转元,保留两位小数', () => {
    expect(formatPrice(12345)).toBe('123.45')
  })

  it('处理 0 和负数', () => {
    expect(formatPrice(0)).toBe('0.00')
    expect(formatPrice(-500)).toBe('-5.00')
  })

  it('非法输入抛错', () => {
    expect(() => formatPrice(NaN)).toThrow()
  })
})

describe 分组、it 写用例、expect 断言——和 Jest 一模一样的 API。

组件测试:@vue/test-utils

测一个计数器组件:

// components/Counter.test.ts
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'

describe('Counter', () => {
  it('点击按钮计数加一', async () => {
    const wrapper = mount(Counter)

    expect(wrapper.text()).toContain('0')

    await wrapper.find('button').trigger('click')

    expect(wrapper.text()).toContain('1')
  })

  it('接受初始值 prop', () => {
    const wrapper = mount(Counter, {
      props: { initial: 10 },
    })
    expect(wrapper.text()).toContain('10')
  })
})

注意 trigger 后要 await——DOM 更新是异步的,这是新手最常忘的点。

外部依赖:mock 掉

组件里调了接口怎么办?mock:

import { vi } from 'vitest'
import { fetchUsers } from '@/api'

// 整个模块 mock
vi.mock('@/api', () => ({
  fetchUsers: vi.fn(),
}))

it('加载用户列表', async () => {
  // 控制 mock 的返回值
  vi.mocked(fetchUsers).mockResolvedValue([
    { id: 1, name: '张三' },
  ])

  const wrapper = mount(UserList)
  await flushPromises()

  expect(wrapper.text()).toContain('张三')
  expect(fetchUsers).toHaveBeenCalledOnce()
})

vi.fn() 造间谍函数、vi.mock() 替换整个模块、vi.mocked() 拿回类型——测试三件套。

我的经验:测试写在哪个性价比最高

不是所有代码都值得测。我的优先级排序:

  1. 工具函数、数据转换(format、validator)——纯函数,写测试成本最低收益最高,必须测
  2. 核心 composables(useCart、useForm)——业务逻辑集中地
  3. 复杂组件的关键交互——按钮点了状态对不对、表单校验通不通
  4. 简单展示组件——基本不测,靠类型和 review 保证

别追求覆盖率数字。我们项目定的是"核心业务逻辑 80%,全项目不强求"。为了冲 100% 给一行 getter 写测试,纯属自我感动。

CI 集成

GitHub Actions 加一步:

- run: npm run test:run

测试挂了直接拦下 PR。从"烧香式重构"变成"绿灯式重构",安全感完全不同。

小结

  • Vite 项目选 Vitest:复用配置、秒级启动、API 兼容 Jest
  • 组件测试用 @vue/test-utils,trigger 后记得 await
  • 外部依赖 vi.mock 掉,数据请求绝不真的发
  • 测试性价比排序:工具函数 > composables > 复杂组件交互 > 展示组件
  • 覆盖率看核心逻辑,别追数字

补上测试后第一周,CI 就拦住了一个边界条件的回归 bug。那一刻我深刻理解了什么叫"测试是重构的许可证"。

看到这里了?如果这篇文章帮到过你

请我喝瓶可乐就好 🥤

收款码

评论0 条

还没有评论,来抢沙发~

发表评论

评论需审核后显示

© 2026 许冬博客 · 专注 JS 技术分享

·RSS 订阅·Powered by Nuxt 4