Vitest + Vue Test Utils
约 719 字大约 2 分钟
布欧-Lewyon
2026-05-15
首页 › Vue › 测试与性能 › Vitest + Vue Test Utils
工具链
- Vitest:基于 Vite 的测试框架(类似 Jest,但更快)
- @vue/test-utils:Vue 官方测试工具库
安装
npm install vitest @vue/test-utils jsdom -D// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom', // 模拟浏览器环境
globals: true,
include: ['src/**/*.{test,spec}.{ts,js}']
}
})mount vs shallowMount
import { mount, shallowMount } from '@vue/test-utils'
import Parent from './Parent.vue'
// mount:渲染所有子组件(完整渲染)
const wrapper = mount(Parent)
// shallowMount:用存根(stub)替代子组件(只测当前组件)
const wrapper = shallowMount(Parent)推荐:浅层测试用
shallowMount,集成测试用mount。
基本测试
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter.vue', () => {
it('渲染默认值', () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('0')
})
it('点击按钮计数增加', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')
})
})查询元素
<template>
<p data-testid="count">计数: {{ count }}</p>
<button @click="increment">+1</button>
<div v-if="show" class="message">消息内容</div>
</template>// 推荐:用 data-testid 属性查询(与样式和结构解耦)
wrapper.find('[data-testid="count"]')
wrapper.find('button')
wrapper.find('.message') // 不推荐:依赖样式类
wrapper.findAll('li')
// 存在判断
expect(wrapper.find('.message').exists()).toBe(true)Props 测试
<!-- Greeting.vue -->
<script setup>
defineProps({ name: { type: String, default: 'World' } })
</script>
<template><p>Hello, {{ name }}!</p></template>import Greeting from './Greeting.vue'
it('传入 Props', () => {
const wrapper = mount(Greeting, {
props: { name: 'Vue' }
})
expect(wrapper.text()).toBe('Hello, Vue!')
})Emits 测试
it('触发事件', async () => {
const wrapper = mount(Child)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('update')).toBeTruthy()
expect(wrapper.emitted('update')[0]).toEqual([42])
})插槽测试
<template>
<Layout>
<template #header>页头</template>
</Layout>
</template>it('渲染插槽内容', () => {
const wrapper = mount(Layout, {
slots: {
header: '<h1>页头</h1>',
default: '<p>内容</p>',
footer: '<p>页脚</p>'
}
})
expect(wrapper.find('h1').text()).toBe('页头')
})组件状态修改
it('修改响应式数据', async () => {
const wrapper = mount(Form)
// 设置 input 值
const input = wrapper.find('input')
await input.setValue('新的值')
// 设置 checkbox
await wrapper.find('checkbox').setChecked(true)
// 设置 select
await wrapper.find('select').setValue('option2')
})异步组件测试
<!-- async setup -->
<script setup>
const data = await fetch('/api/data').then(r => r.json())
</script>it('异步组件', async () => {
const wrapper = mount(AsyncComponent, {
global: {
stubs: { Suspense: false }
}
})
await wrapper.vm.$nextTick()
// 等待 Suspense 解决
})Composables 测试
// composables/useCounter.ts
export function useCounter(initial = 0) {
const count = ref(initial)
function increment() { count.value++ }
return { count, increment }
}import { useCounter } from './useCounter'
it('Composable', () => {
const { count, increment } = useCounter(10)
expect(count.value).toBe(10)
increment()
expect(count.value).toBe(11)
})模拟 Pinia Store
import { setActivePinia, createPinia } from 'pinia'
beforeEach(() => {
setActivePinia(createPinia())
})
it('使用 store', async () => {
const wrapper = mount(UserProfile)
expect(wrapper.text()).toContain('Alice')
})E2E 测试(Playwright)
npm install @playwright/test -D
npx playwright install// e2e/counter.spec.ts
import { test, expect } from '@playwright/test'
test('计数器 E2E', async ({ page }) => {
await page.goto('http://localhost:5173')
const button = page.getByRole('button', { name: '+1' })
await button.click()
await button.click()
await expect(page.getByTestId('count')).toHaveText('2')
})小结
| 工具 | 用途 |
|---|---|
mount | 完整渲染组件 |
shallowMount | 浅层渲染(存根子组件) |
trigger | 触发 DOM 事件 |
setValue | 设置表单值 |
emitted | 验证 emit 事件 |
slots | 传入插槽内容 |
| Playwright | E2E 测试 |
上一节:Pinia 状态管理 下一节:代码分割与 keep-alive
