前置知识: JavaScript、HTML5、CSS

Vue3 测试策略

2 min中级

组件测试与组合式函数测试

1. 测试工具

npm install -D vitest @vue/test-utils

2. 组件测试

import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';

test('increments counter', async () => {
  const wrapper = mount(Counter);
  expect(wrapper.text()).toContain('0');

  await wrapper.find('button').trigger('click');
  expect(wrapper.text()).toContain('1');
});

3. 组合式函数测试

import { withSetup } from './test-utils';

test('useCounter', () => {
  const { result } = withSetup(() => useCounter(0));
  expect(result.count.value).toBe(0);
  result.increment();
  expect(result.count.value).toBe(1);
});

// withSetup 辅助函数
function withSetup(composable) {
  let result;
  const app = createApp({
    setup() {
      result = composable();
      return () => {};
    },
  });
  app.mount(document.createElement('div'));
  return { result, app };
}

4. 异步测试

test('async data', async () => {
  const wrapper = mount(AsyncComponent, {
    global: {
      plugins: [router],
    },
  });

  // 等待异步操作
  await flushPromises();
  expect(wrapper.text()).toContain('loaded data');
});

5. Mock 与 Stub

const wrapper = mount(Component, {
  global: {
    mocks: { $route: { params: { id: '1' } } },
    stubs: { RouterLink: true, ChildComponent: true },
  },
});

测试工具安装

基本写法:安装 Vitest 与 Vue Test Utils npm install -D vitest @vue/test-utils jsdom

# 测试核心依赖
npm install -D vitest @vue/test-utils jsdom @vitejs/plugin-vue

基本写法:安装 Testing Library npm install -D @testing-library/vue

# 行为驱动测试库
npm install -D @testing-library/vue

Vitest 配置

基本写法:vite.config.ts 配置 test test: { environment: 'jsdom', globals: true }

// 配置 Vitest
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
  plugins: [vue()],
  test: { environment: 'jsdom', globals: true }
});

基本写法:测试脚本 'test': 'vitest'

// package.json
{
  "scripts": { "test": "vitest", "test:run": "vitest run" }
}

组件挂载 mount

基本写法:mount 挂载组件 const <wrapper> = mount(<组件>)

// 创建组件实例
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
const wrapper = mount(Counter);

基本写法:传入 props mount(<组件>, { props: { <字段>: <值> } })

// 测试 props 传递
const wrapper = mount(User, { props: { name: 'Alice' } });

基本写法:传入插槽 mount(<组件>, { slots: { default: '<内容>' } })

// 测试插槽内容
const wrapper = mount(Card, { slots: { default: '<p>内容</p>' } });

shallowMount 浅挂载

基本写法:浅挂载不渲染子组件 const <wrapper> = shallowMount(<组件>)

// 隔离子组件测试
const wrapper = shallowMount(App);

DOM 查询

基本写法:find 查找元素 <wrapper>.find('<选择器>')

// 返回第一个匹配的 DOMWrapper
const btn = wrapper.find('button');

基本写法:findAll 查找多个 <wrapper>.findAll('<选择器>')

// 返回所有匹配元素
const items = wrapper.findAll('.item');

基本写法:findComponent 查找子组件 <wrapper>.findComponent(<组件>)

// 查找子组件实例
const child = wrapper.findComponent(UserCard);

文本与属性断言

基本写法:text 读取文本 <wrapper>.text()

// 断言渲染文本
expect(wrapper.text()).toContain('Hello');

基本写法:attributes 读取属性 <wrapper>.attributes('<属性>')

// 断言属性值
expect(wrapper.find('a').attributes('href')).toBe('/about');

基本写法:classes 读取类名 <wrapper>.classes()

// 断言 CSS 类
expect(wrapper.classes()).toContain('active');

交互测试

基本写法:trigger 触发事件 await <wrapper>.find('button').trigger('click')

// 触发 DOM 事件
const btn = wrapper.find('button');
await btn.trigger('click');

基本写法:触发自定义事件 <wrapper>.trigger('<事件>', <数据>)

// 触发自定义 DOM 事件
await wrapper.find('input').trigger('custom', { detail: 1 });

基本写法:setValue 设置输入值 await <wrapper>.find('input').setValue('<值>')

// 模拟用户输入
await wrapper.find('input').setValue('Alice');

emit 事件测试

基本写法:读取组件 emit 的事件 <wrapper>.emitted('<事件名>')

// 断言触发了事件
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('submit')).toBeTruthy();

基本写法:检查 emit 参数 <wrapper>.emitted('<事件>')[0]

// 断言事件参数
expect(wrapper.emitted('submit')[0]).toEqual([{ name: 'Alice' }]);

props 测试

基本写法:setProps 更新 props await <wrapper>.setProps({ <字段>: <新值> })

// 测试 props 变化效果
await wrapper.setProps({ count: 5 });
expect(wrapper.text()).toContain('5');

基本写法:props 读取 <wrapper>.props('<字段>')

// 读取传入的 props
expect(wrapper.props('count')).toBe(5);

响应式测试

基本写法:nextTick 等待更新 await nextTick()

// 等待响应式更新完成
import { nextTick } from 'vue';
count.value++;
await nextTick();
expect(wrapper.text()).toContain('1');

Composables 测试

基本写法:测试组合式函数 const { <结果> } = use<名称>()

// 直接调用组合式函数
import { useCounter } from './useCounter';
const { count, inc } = useCounter();
inc();
expect(count.value).toBe(1);

基本写法:测试需要生命周期的 Composable test('<用例>', () => { withSetup(() => <调用>); })

// 借助 @vue/test-utils 的 withSetup
import { withSetup } from '@vue/test-utils';
const result = withSetup(() => useCounter());

Store 测试

基本写法:测试 Pinia store const <store> = use<Store>()

// 创建 setActivePinia 后测试
import { setActivePinia, createPinia } from 'pinia';
setActivePinia(createPinia());
const store = useCounterStore();
store.inc();
expect(store.count).toBe(1);

基本写法:测试 action await <store>.<action>()

// 测试异步 action
await store.fetchUser();
expect(store.user).toBeDefined();

Mock 依赖

基本写法:vi.mock 模拟模块 vi.mock('<模块>', () => ({ <函数>: vi.fn() }))

// 模拟 API 模块
vi.mock('./api', () => ({
  getUser: vi.fn(() => Promise.resolve({ name: 'Mock' }))
}));

基本写法:mock 实现 vi.fn().mockResolvedValue(<值>)

// 模拟返回值
const mockFn = vi.fn().mockResolvedValue({ ok: true });

基本写法:断言 mock 被调用 expect(<mock>).toHaveBeenCalledWith(<参数>)

// 验证调用
expect(mockFn).toHaveBeenCalledWith('Alice');

路由测试

基本写法:创建测试用 router const <router> = createRouter({ history: createMemoryHistory(), routes })

// 使用 memory history 测试
import { createRouter, createMemoryHistory } from 'vue-router';
const router = createRouter({
  history: createMemoryHistory(),
  routes: [{ path: '/', component: Home }]
});

基本写法:挂载时注入 router mount(<组件>, { global: { plugins: [<router>] } })

// 通过 global.plugins 注入
const wrapper = mount(App, { global: { plugins: [router] } });

provide inject 测试

基本写法:挂载时提供 inject 值 mount(<组件>, { global: { provide: { <key>: <值> } } })

// 提供 inject 依赖
const wrapper = mount(Child, {
  global: { provide: { theme: 'dark' } }
});

快照测试

基本写法:toMatchSnapshot 匹配快照 expect(<wrapper>.html()).toMatchSnapshot()

// 保存组件 HTML 快照
expect(wrapper.html()).toMatchSnapshot();

基本写法:更新快照 vitest -u

# 更新过期快照
npx vitest -u

覆盖率

基本写法:启用覆盖率 vitest run --coverage

# 收集测试覆盖率
npx vitest run --coverage

基本写法:配置覆盖率 coverage: { provider: 'v8', reporter: ['text', 'html'] }

// vite.config.ts
test: {
  coverage: { provider: 'v8', reporter: ['text', 'html'] }
}

异步测试

基本写法:等待异步更新 await <wrapper>.vm.$nextTick()

// 等待 Vue 更新
await wrapper.vm.$nextTick();

基本写法:flushPromises 等待微任务 await flushPromises()

// 等待所有 Promise
import { flushPromises } from '@vue/test-utils';
await flushPromises();

测试生命周期

基本写法:beforeEach 每个用例前执行 beforeEach(() => <初始化>)

// 重置状态
beforeEach(() => {
  setActivePinia(createPinia());
});

测试命名约定

基本写法:测试文件命名 <组件>.spec.ts 或 <组件>.test.ts

// 推荐与组件同目录
// src/components/Counter.spec.ts

describe 分组

基本写法:分组相关用例 describe('<分组>', () => { it('<用例>', () => {}) })

// 组织测试用例
describe('Counter', () => {
  it('初始值为 0', () => {
    const wrapper = mount(Counter);
    expect(wrapper.text()).toContain('0');
  });
});