Files
2026-07-13 21:37:07 +08:00

5.9 KiB
Raw Permalink Blame History

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

组件测试

基本用法

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

it('应用 variant 类名', () => {
  const wrapper = mount(Button, {
    props: { variant: 'primary' }
  })
  expect(wrapper.classes()).toContain('btn-primary')
})

事件触发

it('触发 update 事件并携带载荷', async () => {
  const wrapper = mount(Input)
  await wrapper.find('input').setValue('新值')
  expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['新值'])
})

插槽

it('渲染具名插槽', () => {
  const wrapper = mount(Card, {
    slots: {
      header: '<h1>标题</h1>',
      default: '<p>内容</p>'
    }
  })
  expect(wrapper.html()).toContain('<h1>标题</h1>')
})

组合式函数测试

直接调用,无需挂载:

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

工具函数测试

最简单——纯函数:

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

组合式函数:

import { vi } from 'vitest'

vi.mock('./useAuth', () => ({
  useAuth: vi.fn(() => ({
    user: { id: 1, name: 'Test' },
    isAuthenticated: true
  }))
}))

API 调用:

global.fetch = vi.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ data: [] })
  })
)

Router Mock

在组件测试中 Mock useRouteuseRouter

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

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 测试异步组件:

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

it('传送模态框内容', () => {
  const wrapper = mount(Modal, {
    global: {
      stubs: {
        teleport: true, // 将 teleport 替换为桩,使其内联渲染
      },
    },
  })

  expect(wrapper.text()).toContain('模态框内容')
})

访问被传送的内容:

it('查找被传送的内容', () => {
  document.body.innerHTML = '<div id="modal-target"></div>'

  mount(Modal, { props: { open: true } })

  // 内容被传送到 #modal-target
  expect(document.body.innerHTML).toContain('模态框内容')
})

最佳实践

应做:

  • 测试行为(用户看到/操作的内容),而非实现细节
  • 采用 Arrange-Act-Assert(准备—执行—断言)结构
  • 每个测试只做一个断言
  • 使用描述性的测试名称
  • Mock 外部依赖

不应做:

  • 测试 Vue 内部机制(响应式系统)
  • 测试第三方库
  • 测试琐碎的 getter/setter
  • 测试实现细节

测试内容

需要测试:

  • 用户交互(点击、输入)
  • 条件渲染
  • Props 验证、触发的事件
  • 计算属性、业务逻辑

跳过:

  • Vue 内部机制、第三方库
  • 琐碎的 getter/setter
  • 实现细节

运行

pnpm test                          # 全部
pnpm exec vitest Button.spec.ts    # 指定文件
pnpm exec vitest --watch           # 监听模式
pnpm test --coverage               # 覆盖率

文档: vitest.dev · test-utils.vuejs.org