# Vue 测试
Vue 3 组件、组合式函数和工具函数的测试模式。
## 快速参考
| 测试类型 | 模式 |
| ---------------- | ------------------------------------- |
| 组件 | `mount(Component, { props, slots })` |
| 用户交互 | `await wrapper.trigger('click')` |
| 触发的事件 | `wrapper.emitted('update')` |
| 组合式函数 | 直接调用,测试返回值 |
| 工具函数 | 纯函数测试(最简单) |
## 技术栈
- **Vitest** — 测试运行器
- **@vue/test-utils** — 组件挂载、交互
- **@testing-library/vue** — 以用户为中心的替代方案
- **happy-dom / jsdom** — DOM 环境
## 文件位置
将测试文件与代码放在一起:
```
Button.vue → Button.spec.ts
useAuth.ts → useAuth.spec.ts
formatters.ts → formatters.spec.ts
```
## 组件测试
### 基本用法
```ts
import { mount } from '@vue/test-utils'
import Button from './Button.vue'
it('渲染插槽', () => {
const wrapper = mount(Button, {
slots: { default: '点击我' }
})
expect(wrapper.text()).toBe('点击我')
})
it('点击时触发事件', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
})
```
### Props
```ts
it('应用 variant 类名', () => {
const wrapper = mount(Button, {
props: { variant: 'primary' }
})
expect(wrapper.classes()).toContain('btn-primary')
})
```
### 事件触发
```ts
it('触发 update 事件并携带载荷', async () => {
const wrapper = mount(Input)
await wrapper.find('input').setValue('新值')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['新值'])
})
```
### 插槽
```ts
it('渲染具名插槽', () => {
const wrapper = mount(Card, {
slots: {
header: '
标题
',
default: '内容
'
}
})
expect(wrapper.html()).toContain('标题
')
})
```
## 组合式函数测试
直接调用,无需挂载:
```ts
import { useCounter } from './useCounter'
it('递增计数', () => {
const { count, increment } = useCounter(0)
expect(count.value).toBe(0)
increment()
expect(count.value).toBe(1)
})
it('重置为初始值', () => {
const { count, increment, reset } = useCounter(5)
increment()
increment()
expect(count.value).toBe(7)
reset()
expect(count.value).toBe(5)
})
```
## 工具函数测试
最简单——纯函数:
```ts
import { formatCurrency, slugify } from './formatters'
describe('formatCurrency', () => {
it('格式化美元金额', () => {
expect(formatCurrency(10.5)).toBe('$10.50')
})
})
describe('slugify', () => {
it('转换为小写', () => {
expect(slugify('Hello World')).toBe('hello-world')
})
it('移除特殊字符', () => {
expect(slugify('Hello! World?')).toBe('hello-world')
})
})
```
## Mock
**组合式函数:**
```ts
import { vi } from 'vitest'
vi.mock('./useAuth', () => ({
useAuth: vi.fn(() => ({
user: { id: 1, name: 'Test' },
isAuthenticated: true
}))
}))
```
**API 调用:**
```ts
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: [] })
})
)
```
## Router Mock
在组件测试中 Mock `useRoute` 和 `useRouter`:
```ts
import { vi } from 'vitest'
import { mount } from '@vue/test-utils'
vi.mock('vue-router', () => ({
useRoute: vi.fn(() => ({
params: { id: '123' },
query: { filter: 'active' },
path: '/users/123',
})),
useRouter: vi.fn(() => ({
push: vi.fn(),
replace: vi.fn(),
})),
}))
it('使用路由参数', () => {
const wrapper = mount(UserPage)
expect(wrapper.text()).toContain('123')
})
```
**每个测试的动态路由 Mock:**
```ts
import { useRoute } from 'vue-router'
it('处理不同的路由', () => {
vi.mocked(useRoute).mockReturnValue({
params: { id: '456' },
} as any)
const wrapper = mount(UserPage)
expect(wrapper.text()).toContain('456')
})
```
## Suspense 与 Teleport
**使用 Suspense 测试异步组件:**
```ts
import { flushPromises, mount } from '@vue/test-utils'
it('渲染异步内容', async () => {
const wrapper = mount(AsyncComponent, {
global: {
stubs: { Suspense: false }, // 不对 Suspense 进行桩替换
},
})
// 等待异步 setup 完成
await flushPromises()
expect(wrapper.text()).toContain('已加载内容')
})
```
**测试 Teleport:**
```ts
it('传送模态框内容', () => {
const wrapper = mount(Modal, {
global: {
stubs: {
teleport: true, // 将 teleport 替换为桩,使其内联渲染
},
},
})
expect(wrapper.text()).toContain('模态框内容')
})
```
**访问被传送的内容:**
```ts
it('查找被传送的内容', () => {
document.body.innerHTML = ''
mount(Modal, { props: { open: true } })
// 内容被传送到 #modal-target
expect(document.body.innerHTML).toContain('模态框内容')
})
```
## 最佳实践
**应做:**
- 测试行为(用户看到/操作的内容),而非实现细节
- 采用 Arrange-Act-Assert(准备—执行—断言)结构
- 每个测试只做一个断言
- 使用描述性的测试名称
- Mock 外部依赖
**不应做:**
- 测试 Vue 内部机制(响应式系统)
- 测试第三方库
- 测试琐碎的 getter/setter
- 测试实现细节
## 测试内容
**需要测试:**
- 用户交互(点击、输入)
- 条件渲染
- Props 验证、触发的事件
- 计算属性、业务逻辑
**跳过:**
- Vue 内部机制、第三方库
- 琐碎的 getter/setter
- 实现细节
## 运行
```bash
pnpm test # 全部
pnpm exec vitest Button.spec.ts # 指定文件
pnpm exec vitest --watch # 监听模式
pnpm test --coverage # 覆盖率
```
**文档:** [vitest.dev](https://vitest.dev/) · [test-utils.vuejs.org](https://test-utils.vuejs.org/)