单元测试:Vitest 与 Vue Test Utils
单元测试是确保 Vue 3 项目代码质量的重要环节,能够验证组件逻辑、状态管理和交互行为的正确性。Vitest 和 Vue Test Utils 是 Vue 3 生态中推荐的测试工具,前者提供快速的测试运行环境,后者为测试 Vue 组件提供便捷的 API。本节将介绍这两者的安装、配置和基本使用方法,并通过示例展示如何进行单元测试,帮助你建立可靠的测试流程。
什么是单元测试?
单元测试是对代码最小单元(如函数、组件)的独立测试,旨在验证其在各种输入下的行为是否符合预期。在 Vue 3 中,单元测试通常针对组件的逻辑和渲染结果。
为什么使用 Vitest 和 Vue Test Utils?
- Vitest:
- 由 Vite 团队开发,基于 Vite 的快速构建能力。
- 支持 ESM、TypeScript 和 HMR。
- 与 Jest 兼容,易于迁移。
- Vue Test Utils:
- 官方测试工具,提供组件挂载、模拟和断言 API。
- 支持Vue 3 的 Composition API。
安装与配置
安装
在 Vue 3 项目中添加测试依赖:
npm install -D vitest @vue/test-utils jsdom happy-dom
配置 Vitest
vite.config.ts
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom', // 模拟浏览器环境
globals: true, // 全局 API(如 describe、it)
setupFiles: './tests/setup.ts' // 全局配置
}
});
tests/setup.ts
// tests/setup.ts
import { config } from '@vue/test-utils';
// 全局配置(可选)
config.global.mocks = {
$t: (msg: string) => msg // 模拟 i18n
};
package.json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}
项目结构
src/
├── components/
│ └── Counter.vue
├── tests/
│ ├── setup.ts
│ └── Counter.test.ts
├── vite.config.ts
└── package.json
基本使用
测试组件:Counter.vue
<!-- src/components/Counter.vue -->
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'Counter',
props: {
initial: {
type: Number,
default: 0
}
},
setup(props) {
const count = ref(props.initial);
const increment = () => count.value++;
return { count, increment };
}
});
</script>
编写单元测试
Counter.test.ts
// tests/Counter.test.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from '../components/Counter.vue';
describe('Counter.vue', () => {
it('renders initial count', () => {
const wrapper = mount(Counter, {
props: { initial: 5 }
});
expect(wrapper.text()).toContain('计数: 5');
});
it('increments count when button is clicked', async () => {
const wrapper = mount(Counter);
const button = wrapper.find('button');
await button.trigger('click');
expect(wrapper.text()).toContain('计数: 1');
});
it('has correct component name', () => {
const wrapper = mount(Counter);
expect(wrapper.vm.$options.name).toBe('Counter');
});
});
运行测试
npm run test
- 输出:
✓ tests/Counter.test.ts (3) ✓ Counter.vue ✓ renders initial count ✓ increments count when button is clicked ✓ has correct component name
实时测试
npm run test:watch
- 效果:修改代码后自动重新运行测试。
核心测试技巧
1. 测试 Props
it('renders with custom initial value', () => {
const wrapper = mount(Counter, {
props: { initial: 10 }
});
expect(wrapper.text()).toContain('计数: 10');
});
2. 测试事件
it('emits event on increment', async () => {
const wrapper = mount(Counter);
await wrapper.vm.increment();
expect(wrapper.emitted('increment')).toBeUndefined(); // 无自定义事件
expect(wrapper.text()).toContain('计数: 1');
});
- 扩展:若需要测试自定义事件,添加
emits:<script lang="ts"> export default defineComponent({ emits: ['increment'], setup(props, { emit }) { const count = ref(0); const increment = () => { count.value++; emit('increment', count.value); }; return { count, increment }; } }); </script>
3. 测试异步逻辑
<!-- AsyncComp.vue -->
<template>
<p>{{ data }}</p>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
async setup() {
const data = ref<string>('');
const response = await new Promise<string>(resolve =>
setTimeout(() => resolve('Loaded'), 100)
);
data.value = response;
return { data };
}
});
</script>
// tests/AsyncComp.test.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import AsyncComp from '../components/AsyncComp.vue';
describe('AsyncComp.vue', () => {
it('renders async data', async () => {
const wrapper = mount(AsyncComp);
expect(wrapper.text()).toBe(''); // 初始状态
await new Promise(resolve => setTimeout(resolve, 150)); // 等待异步
expect(wrapper.text()).toBe('Loaded');
});
});
4. 模拟用户交互
it('updates count on multiple clicks', async () => {
const wrapper = mount(Counter);
const button = wrapper.find('button');
await button.trigger('click');
await button.trigger('click');
expect(wrapper.text()).toContain('计数: 2');
});
高级技巧
1. Mocking 依赖
- 模拟全局对象:
it('uses mocked $t', () => { const wrapper = mount(Counter, { global: { mocks: { $t: (msg: string) => `Translated: ${msg}` } } }); expect(wrapper.vm.$t('test')).toBe('Translated: test'); });
2. 测试 Pinia Store
// src/stores/counter.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
});
// tests/CounterWithStore.test.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import Counter from '../components/Counter.vue';
describe('Counter with Pinia', () => {
it('uses store count', async () => {
setActivePinia(createPinia());
const wrapper = mount(Counter);
const button = wrapper.find('button');
await button.trigger('click');
expect(wrapper.text()).toContain('计数: 1');
});
});
注意事项
- 环境配置:
jsdom模拟 DOM,复杂交互可能需happy-dom。
- 异步测试:
- 确保等待异步操作完成(如
await wrapper.vm.$nextTick())。
- 确保等待异步操作完成(如
- 覆盖率:
- 添加
--coverage查看测试覆盖率:npm run test -- --coverage
- 添加
总结
Vitest 和 Vue Test Utils 为 Vue 3 提供了快速、强大的单元测试能力。通过本节,你学会了安装配置、测试组件逻辑和交互,并掌握了高级技巧如 Mocking 和 Pinia 集成。这些技能确保了代码的健壮性。下一节将探讨 E2E 测试,带你进入更全面的测试领域!
