Vue Test Utils
什么是 Vue Test Utils?
Vue Test Utils 是 Vue.js 官方的测试工具库,用于测试 Vue 组件。它提供了一系列 API 来挂载组件、模拟用户交互、断言组件行为等,让开发者可以轻松编写组件测试。
1. 安装 Vue Test Utils
使用 npm 或 yarn 安装 Vue Test Utils 和测试框架(如 Jest)。
示例:安装 Vue Test Utils
# 使用 npm
npm install --save-dev @vue/test-utils@next jest @vue/vue3-jest babel-jest
# 使用 yarn
yarn add --dev @vue/test-utils@next jest @vue/vue3-jest babel-jest
2. 基本配置
创建 Jest 配置文件,配置 Vue 组件的测试环境。
示例:Jest 配置
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\.js$': 'babel-jest'
},
moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node']
}
3. 基本测试结构
编写一个基本的组件测试。
示例:基本测试结构
// 组件:HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script setup>
const props = defineProps({
msg: {
type: String,
default: 'Hello World'
}
})
</script>
// 测试文件:HelloWorld.spec.js
import { mount } from '@vue/test-utils'
import HelloWorld from './HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'New Message'
const wrapper = mount(HelloWorld, {
props: {
msg
}
})
// 断言组件渲染了正确的内容
expect(wrapper.text()).toMatch(msg)
})
it('renders default message when no props passed', () => {
const wrapper = mount(HelloWorld)
// 断言组件渲染了默认内容
expect(wrapper.text()).toMatch('Hello World')
})
})
4. 挂载组件
使用 mount() 函数挂载组件,创建一个包含组件实例的 wrapper 对象。
示例:挂载组件
import { mount } from '@vue/test-utils'
import MyComponent from './MyComponent.vue'
// 基本挂载
const wrapper = mount(MyComponent)
// 传递 props
const wrapper = mount(MyComponent, {
props: {
title: 'Test Title',
count: 10
}
})
// 传递 slots
const wrapper = mount(MyComponent, {
slots: {
default: 'Default Slot Content',
header: '<h2>Header Slot</h2>',
footer: '<p>Footer Slot</p>'
}
})
// 传递 provide
const wrapper = mount(MyComponent, {
global: {
provide: {
'my-inject-key': 'injected value'
}
}
})
// 模拟全局属性
const wrapper = mount(MyComponent, {
global: {
mocks: {
$t: (key) => key, // 模拟 i18n
$router: { push: jest.fn() } // 模拟 vue-router
}
}
})
5. 查找元素
使用 wrapper 对象的方法查找组件内的元素。
示例:查找元素
// 按选择器查找
const button = wrapper.find('button')
const input = wrapper.find('.my-input')
const listItem = wrapper.find('li:nth-child(2)')
// 按组件名查找
const childComponent = wrapper.findComponent(ChildComponent)
// 查找所有匹配的元素
const buttons = wrapper.findAll('button')
const listItems = wrapper.findAll('li')
// 按文本内容查找
const link = wrapper.find('a', { text: 'Learn More' })
// 使用数据属性查找
const submitButton = wrapper.find('[data-testid="submit-button"]')
6. 模拟用户交互
使用 wrapper 对象的方法模拟用户交互,如点击、输入、提交等。
示例:模拟用户交互
// 点击元素
const button = wrapper.find('button')
await button.trigger('click')
// 输入文本
const input = wrapper.find('input')
await input.setValue('Test Input')
// 触发键盘事件
await input.trigger('keydown.enter')
// 触发表单提交
const form = wrapper.find('form')
await form.trigger('submit.prevent')
// 触发自定义事件
const customElement = wrapper.find('.custom-element')
await customElement.trigger('custom-event', { detail: { value: 123 } })
7. 断言组件行为
使用断言库(如 Jest)断言组件的行为是否符合预期。
示例:断言组件行为
// 断言文本内容
expect(wrapper.text()).toContain('Hello World')
// 断言元素存在
expect(wrapper.find('button').exists()).toBe(true)
// 断言元素不存在
expect(wrapper.find('.non-existent').exists()).toBe(false)
// 断言元素可见
expect(wrapper.find('.visible-element').isVisible()).toBe(true)
// 断言元素属性
const input = wrapper.find('input')
expect(input.attributes('type')).toBe('text')
expect(input.attributes('placeholder')).toBe('Enter text')
// 断言元素类名
const button = wrapper.find('button')
expect(button.classes()).toContain('primary')
// 断言元素样式
const element = wrapper.find('.styled-element')
expect(element.style('color')).toBe('red')
// 断言表单值
const input = wrapper.find('input')
await input.setValue('Test')
expect(input.element.value).toBe('Test')
// 断言组件 props
const childComponent = wrapper.findComponent(ChildComponent)
expect(childComponent.props('title')).toBe('Test Title')
// 断言事件触发
const button = wrapper.find('button')
await button.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
expect(wrapper.emitted('update:count')).toEqual([[1], [2], [3]])
8. 测试组合式 API 组件
测试使用组合式 API 编写的组件。
示例:测试组合式 API 组件
// 组件:Counter.vue
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
// 暴露给测试的属性
defineExpose({
count,
increment
})
</script>
// 测试文件:Counter.spec.js
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter.vue', () => {
it('increments count when button is clicked', async () => {
const wrapper = mount(Counter)
const button = wrapper.find('button')
// 初始值应为 0
expect(wrapper.text()).toMatch('Count: 0')
// 点击按钮
await button.trigger('click')
// 值应变为 1
expect(wrapper.text()).toMatch('Count: 1')
// 再次点击按钮
await button.trigger('click')
// 值应变为 2
expect(wrapper.text()).toMatch('Count: 2')
})
it('can access exposed properties', () => {
const wrapper = mount(Counter)
// 访问暴露的属性
expect(wrapper.vm.count).toBe(0)
// 调用暴露的方法
wrapper.vm.increment()
expect(wrapper.vm.count).toBe(1)
})
})
9. 测试异步组件
测试包含异步操作的组件,如 API 请求、定时器等。
示例:测试异步组件
// 组件:AsyncComponent.vue
<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>Data: {{ data }}</div>
<button @click="fetchData">Fetch Data</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const loading = ref(false)
const error = ref(null)
const data = ref(null)
async function fetchData() {
loading.value = true
error.value = null
try {
// 模拟 API 请求
const response = await fetch('https://api.example.com/data')
const result = await response.json()
data.value = result
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
</script>
// 测试文件:AsyncComponent.spec.js
import { mount } from '@vue/test-utils'
import AsyncComponent from './AsyncComponent.vue'
describe('AsyncComponent.vue', () => {
it('fetches data when button is clicked', async () => {
// 模拟 fetch API
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve('Test Data')
})
)
const wrapper = mount(AsyncComponent)
const button = wrapper.find('button')
// 初始状态
expect(wrapper.text()).toContain('Fetch Data')
expect(wrapper.find('.loading').exists()).toBe(false)
// 点击按钮
await button.trigger('click')
// 加载状态
expect(wrapper.text()).toContain('Loading...')
// 等待异步操作完成
await wrapper.vm.$nextTick()
// 数据加载完成
expect(wrapper.text()).toContain('Data: Test Data')
expect(global.fetch).toHaveBeenCalledTimes(1)
})
it('handles error when fetch fails', async () => {
// 模拟 fetch API 失败
global.fetch = jest.fn(() =>
Promise.reject(new Error('Network Error'))
)
const wrapper = mount(AsyncComponent)
const button = wrapper.find('button')
// 点击按钮
await button.trigger('click')
// 等待异步操作完成
await wrapper.vm.$nextTick()
// 错误状态
expect(wrapper.text()).toContain('Error: Network Error')
})
})
10. 测试路由组件
测试使用 Vue Router 的组件。
示例:测试路由组件
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import { createApp } from 'vue'
import RouteComponent from './RouteComponent.vue'
// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: RouteComponent },
{ path: '/about', component: () => import('./AboutComponent.vue') }
]
})
// 创建应用实例
const app = createApp({})
app.use(router)
// 测试路由组件
describe('RouteComponent.vue', () => {
it('renders route links', () => {
const wrapper = mount(RouteComponent, {
global: {
plugins: [router]
}
})
// 断言路由链接存在
expect(wrapper.find('router-link[to="/"]').exists()).toBe(true)
expect(wrapper.find('router-link[to="/about"]').exists()).toBe(true)
})
it('renders router-view', async () => {
const wrapper = mount({
template: '<router-view />',
}, {
global: {
plugins: [router]
}
})
// 导航到首页
await router.push('/')
await router.isReady()
// 断言首页内容渲染
expect(wrapper.text()).toContain('Home Page')
// 导航到关于页
await router.push('/about')
await router.isReady()
// 断言关于页内容渲染
expect(wrapper.text()).toContain('About Page')
})
})
11. Vue Test Utils 最佳实践
- 使用
data-testid属性标识测试元素,避免依赖 CSS 类名或结构 - 测试组件的公共 API,而不是内部实现细节
- 使用
defineExpose暴露需要测试的内部属性和方法 - 模拟外部依赖(如 API 请求、路由、状态管理)
- 编写清晰、简洁的测试用例,每个测试只关注一个功能点
- 使用异步测试处理组件中的异步操作
- 测试组件的各种状态(初始状态、加载状态、错误状态、成功状态)
- 使用快照测试验证组件的渲染结果