Vitest 测试实践

为什么选择Vitest

Jest 与 Vitest对比

Jest Vitest
构建基础 Webpack / Babel 原生 ESM,基于 Vite
启动速度 较慢(需打包,但提供了缓存机制) 极快(无需打包,Vite 的快速冷启动,按需加载)
内存占用
语法支持 需要额外的配置来支持现代 JavaScript(例如 ES Modules、TypeScript)需要安装额外的转换器(如 Babel 或 ts-jest) 不需要额外适配
调试体验 一般 Watch和热更新,可直接在浏览器/VS Code 中调试
社区生态 成熟,插件丰富 快速发展,Vue/React 生态支持良好

所以,对于使用 Vite 的项目,Vitest 是更现代、更高效的选择

⚠️ 但 Jest 仍适用于 Node.js 服务端或复杂兼容性场景

使用

核心测试工具

exprect

expect 断言:验证行为是否符合预期

1
2
3
4
5
6
7
8
// 检查字符串是否包含子串
expect("Hello World").toContain("World");

// 检查数组是否包含某元素
expect([1, 2, 3]).toContain(2);

// 检查是否被调用
expect(mockFn).toHaveBeenCalled();

更多方法

匹配器 功能描述
toBe(value) 检查值是否严格等于(===)指定的值,通常用于原始类型(如数字、字符串)。
toEqual(value) 检查对象或数组的内容是否深度相等。
toContain(item) 检查数组或可迭代对象是否包含指定的元素。
toContainEqual(item) 检查数组或可迭代对象是否包含与指定值深度相等的元素。
toBeTruthy() 检查值是否为“真值”(truthy),即在布尔上下文中为 true。
toBeFalsy() 检查值是否为“假值”(falsy),即在布尔上下文中为 false。
toBeNull() 检查值是否为 null。
toBeUndefined() 检查值是否为 undefined。
toBeDefined() 检查值是否不是 undefined。
toBeNaN() 检查值是否为 NaN。
toBeGreaterThan(number) 检查值是否大于指定的数字。
toBeGreaterThanOrEqual(number) 检查值是否大于或等于指定的数字。
toBeLessThan(number) 检查值是否小于指定的数字。
toBeLessThanOrEqual(number) 检查值是否小于或等于指定的数字。
toHaveLength(number) 检查数组、字符串或其他可迭代对象的长度是否等于指定值。
toThrow(error?) 检查函数是否抛出错误,可以指定错误消息或错误类型。
toMatch(regexpOrString) 检查字符串是否匹配指定的正则表达式或子字符串。
toMatchObject(object) 检查对象是否部分匹配指定的对象结构。
toHaveProperty(keyPath, value?) 检查对象是否具有指定的属性,可以进一步检查属性值。
toBeInstanceOf(Class) 检查对象是否是某个类的实例。
toBeCloseTo(number, numDigits?) 检查浮点数是否接近指定值,避免精度问题。
toHaveBeenCalled() 检查模拟函数是否被调用过。
toHaveBeenCalledTimes(number) 检查模拟函数是否被调用了指定次数。
toHaveBeenCalledWith(…args) 检查模拟函数是否被调用时传入了指定的参数。
toHaveBeenLastCalledWith(…args) 检查模拟函数最后一次调用时是否传入了指定的参数。
toHaveBeenNthCalledWith(n, …args) 检查模拟函数第 n 次调用时是否传入了指定的参数。
toHaveReturned() 检查模拟函数是否成功返回(没有抛出错误)。
toHaveReturnedTimes(number) 检查模拟函数是否成功返回了指定次数。
toHaveReturnedWith(value) 检查模拟函数是否返回了指定的值。
toHaveLastReturnedWith(value) 检查模拟函数最后一次调用是否返回了指定的值。
toHaveNthReturnedWith(n, value) 检查模拟函数第 n 次调用是否返回了指定的值。
toBeCalled() 等同于 toHaveBeenCalled(),检查模拟函数是否被调用过。
toBeCalledTimes(number) 等同于 toHaveBeenCalledTimes(number),检查模拟函数是否被调用了指定次数。
toBeCalledWith(…args) 等同于 toHaveBeenCalledWith(…args),检查模拟函数是否被调用时传入了指定参数。
lastCalledWith(…args) 等同于 toHaveBeenLastCalledWith(…args),检查模拟函数最后一次调用时是否传入了指定参数。

mount

mount:完整挂载组件进行测试

mount 不返回 JSX 虚拟节点,而是返回一个 Wrapper 对象,用于操作和断言组件行为。

1
2
3
4
5
6
7
8
9
10
11
import { mount } from '@vue/test-utils'
import ButtonPlus from './ButtonPlus.vue'

const wrapper = mount(ButtonPlus, { // mount:@vue/test-utils 提供的一个方法,用于将 Vue 组件挂载到一个虚拟 DOM 中(通常是在 jsdom [提供浏览器环境])
props: {
type: 'interactive'
},
slots: {
default: '点击我'
}
})

wrapper 提供以下常用方法:

  • wrapper.text():获取渲染文本
  • wrapper.classes():获取类名数组
  • wrapper.find():查找 DOM 元素
  • wrapper.trigger():触发事件
  • wrapper.emitted():检查事件是否被触发

推荐使用 mount 而非 shallowMount,以测试真实渲染结果和子组件交互。

编写高质量测试用例

与ai结合

先使用AI编写一套 提问模板

包括角色身份定位(产品经理)、需求背景输出规范

比如我在开发组件库的按钮👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 身份定位

- **角色**:互联网产品经理
- **目标**:产品需求分析和功能点设计

# 需求

以"[XXX]"形式定义变量用于对话中不同任务的触发指令
以"/help" 为触发关键词,列出所有定义的变量`**XXX**`以及代表的任务

对话过程用中文交流,专业术语可用英文或缩写。

- [XQFX]:(需求分析) 根据给出的内容输出需求分析文档(md)
- [GNSJ]:(功能设计) 以上文中的 "需求分析文档" 为依据

# 背景

(项目文档地址:https://xxx/button.com )

首次可补充提问来完善背景

# 输出规范

- **需求分析**[XQFX]
- **格式**:用户调研摘要、竞品对比报告、市场趋势分析。
- **内容**:用户痛点、期望功能、安全性需求。
- **功能点设计**[GNSJ]
- **格式**:功能描述、api 设计、交互关系。
- **内容**:功能实现细节、用户操作流程、异常处理。

# 示例指令

- **需求分析**:[XQFX]组件库按钮组件。
- **功能点设计**:[GNSJ]

请在后续对话中使用上述结构和示例指令来指导任务执行。

接着 [XQFX][GNSJ],AI会返回给你 需求分析与功能设计文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
好的,以下是“Button | Leatom”组件的需求分析和功能设计文档:

---

# 需求分析文档:Button | Leatom 组件

## 一、用户调研摘要

### 用户痛点
......

# 功能点设计文档:ButtonPlus | Veyra 组件

## 一、功能描述

### 1.1 功能目标
设计一个具有丰富视觉特效和高度可定制性的按钮组件,以满足不同用户的需求,同时提升开发效率。

### 1.2 功能范围
......

## 三、交互关系
......

---

以上是“Button | Leatom”组件的需求分析和功能设计文档。如果需要进一步调整或补充,请随时告知。

再把AI生成的 需求分析文档 再以丢给新的对话

你是一个优秀的测开工程师,现在需要你根据我给你的需求分析帮我写测试用例

要求:vitest 技术栈,mount 尽量返回 JSX 虚拟节点的形式(更贴近实际的渲染结果,代码也更加简洁直观),it,test 等方法的 name 属性用中文描述(英文更规范,但是我英语差),以下是需求文档内容

结果返回就是测试用例了

1
2
3
4
5
6
test('renders button with correct text', () => {
......
})
test('......', () => {
......
})

测试覆盖率

覆盖率类型说明

类型 说明
行覆盖率(Line Coverage) 一行代码是否被执行
语句覆盖率(Statement Coverage) 每个独立语句是否执行
分支覆盖率(Branch Coverage) 每个 if/else、三元运算符等分支是否覆盖
函数覆盖率(Function Coverage) 每个函数是否至少调用一次

img

Vitest 使用 istanbul 报告,可通过 --coverage 生成 HTML 报告

合理设定目标

项目类型 推荐目标
安全关键模块(支付、权限) ≥ 90% 分支覆盖率
核心业务组件 ≥ 80% 行覆盖率
普通 UI 组件 ≥ 70% 行覆盖率

避免“为了覆盖而覆盖”,无意义的测试会降低维护成本

保障高质量测试的策略

分层测试策略

层级 目标 工具
单元测试 验证单个组件/函数 Vitest
集成测试 模块间协作 Vitest + Mock
E2E 测试 用户完整流程 Cypress / Playwright
回归测试 修改后不引入新 bug CI 自动运行

持续集成(CI)自动化

1
2
3
4
5
6
7
8
# .github/workflows/test.yml
- name: Run Tests
run: pnpm test --coverage
- name: Upload Coverage
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: coverage/

每次 PR 自动运行测试,覆盖率下降时告警