Vue 3 与 Vitest 高级测试

概述

Vitest 是一款基于 Vite 构建的现代化测试框架,专为 Vue 3 生态系统设计,提供了快速、高效的测试体验。它支持多种测试类型,包括单元测试、集成测试、快照测试等,并且与 Vue 3 的组合式 API 完美兼容。本集将深入探讨 Vitest 的高级特性和最佳实践,帮助你构建全面、可靠的测试套件。

核心知识点

1. Vitest 基础配置

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true, // 启用全局测试 API
    environment: 'jsdom', // 浏览器环境模拟
    setupFiles: './tests/setup.ts', // 测试设置文件
    coverage: {
      provider: 'istanbul', // 覆盖率报告生成器
      reporter: ['text', 'json', 'html'], // 覆盖率报告格式
      include: ['src/**/*'], // 覆盖率统计范围
      exclude: ['node_modules', 'tests'] // 排除目录
    },
    mockReset: true, // 自动重置 mock
    restoreMocks: true, // 恢复 mock 状态
    clearMocks: true // 清除 mock 调用历史
  }
})

2. 测试设置文件

// tests/setup.ts
import { expect, afterEach } from 'vitest'
import { cleanup } from '@testing-library/vue'
import matchers from '@testing-library/jest-dom/matchers'

// 扩展 expect 匹配器
expect.extend(matchers)

// 每个测试后清理 DOM
afterEach(() => {
  cleanup()
})

3. Vue 组件测试

使用 @testing-library/vue 测试 Vue 组件:

<!-- src/components/Counter.vue -->
<template>
  <div>
    <h1>Counter</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}

function decrement() {
  count.value--
}

defineExpose({
  count,
  increment,
  decrement
})
</script>
// tests/components/Counter.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'

describe('Counter Component', () => {
  it('renders correctly', () => {
    const wrapper = mount(Counter)
    expect(wrapper.text()).toContain('Count: 0')
  })

  it('increments count when button is clicked', async () => {
    const wrapper = mount(Counter)
    const button = wrapper.find('button:first-child')
    
    await button.trigger('click')
    expect(wrapper.text()).toContain('Count: 1')
  })

  it('decrements count when button is clicked', async () => {
    const wrapper = mount(Counter)
    const button = wrapper.find('button:last-child')
    
    await button.trigger('click')
    expect(wrapper.text()).toContain('Count: -1')
  })

  it('exposes correct methods and properties', () => {
    const wrapper = mount(Counter)
    expect(wrapper.vm.count).toBe(0)
    wrapper.vm.increment()
    expect(wrapper.vm.count).toBe(1)
  })
})

4. 组合式函数测试

测试 Vue 3 组合式函数:

// src/composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)
  
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  return {
    count,
    doubled,
    increment,
    decrement
  }
}
// tests/composables/useCounter.spec.ts
import { describe, it, expect } from 'vitest'
import { useCounter } from '@/composables/useCounter'

describe('useCounter Composable', () => {
  it('initializes with default value', () => {
    const { count } = useCounter()
    expect(count.value).toBe(0)
  })

  it('initializes with custom value', () => {
    const { count } = useCounter(5)
    expect(count.value).toBe(5)
  })

  it('increments count correctly', () => {
    const { count, increment } = useCounter()
    increment()
    expect(count.value).toBe(1)
  })

  it('decrements count correctly', () => {
    const { count, decrement } = useCounter(2)
    decrement()
    expect(count.value).toBe(1)
  })

  it('computes doubled value correctly', () => {
    const { count, doubled, increment } = useCounter()
    expect(doubled.value).toBe(0)
    
    increment()
    expect(doubled.value).toBe(2)
    
    increment()
    expect(doubled.value).toBe(4)
  })
})

5. Mock 和 Stub

使用 Vitest 的 mock 功能:

// tests/utils/api.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { fetchData } from '@/utils/api'

// Mock 全局 fetch
vi.mock('node-fetch', () => ({
  default: vi.fn()
}))

describe('API Utils', () => {
  it('fetches data correctly', async () => {
    const mockData = { id: 1, name: 'Test' }
    const fetch = await import('node-fetch').then(m => m.default)
    
    fetch.mockResolvedValueOnce({
      json: vi.fn().mockResolvedValueOnce(mockData)
    })
    
    const data = await fetchData('/api/test')
    expect(data).toEqual(mockData)
    expect(fetch).toHaveBeenCalledWith('/api/test')
  })

  it('handles fetch errors', async () => {
    const fetch = await import('node-fetch').then(m => m.default)
    
    fetch.mockRejectedValueOnce(new Error('Network error'))
    
    await expect(fetchData('/api/error')).rejects.toThrow('Network error')
  })
})

6. 异步测试

测试异步操作:

// tests/components/AsyncComponent.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import AsyncComponent from '@/components/AsyncComponent.vue'

describe('AsyncComponent', () => {
  it('fetches and displays data', async () => {
    const mockData = { title: 'Async Data' }
    
    // Mock 组件中的 fetch 方法
    const wrapper = mount(AsyncComponent, {
      global: {
        mocks: {
          $fetch: vi.fn().mockResolvedValue(mockData)
        }
      }
    })
    
    expect(wrapper.text()).toContain('Loading...')
    
    // 等待异步操作完成
    await wrapper.vm.$nextTick()
    
    expect(wrapper.text()).toContain('Async Data')
    expect(wrapper.text()).not.toContain('Loading...')
  })
})

7. 快照测试

使用快照测试确保组件渲染结果一致:

// tests/components/HelloWorld.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld Component', () => {
  it('matches snapshot', () => {
    const wrapper = mount(HelloWorld, {
      props: {
        msg: 'Hello Vitest'
      }
    })
    
    expect(wrapper.html()).toMatchSnapshot()
  })
  
  it('matches updated snapshot with new props', () => {
    const wrapper = mount(HelloWorld, {
      props: {
        msg: 'Updated Message'
      }
    })
    
    expect(wrapper.html()).toMatchSnapshot()
  })
})

8. 测试覆盖范围

配置和查看测试覆盖率:

# 运行测试并生成覆盖率报告
npm run test:coverage

# 查看 HTML 覆盖率报告
open coverage/index.html

9. 环境变量和配置

使用不同环境的测试配置:

// tests/utils/env.spec.ts
import { describe, it, expect } from 'vitest'
import { getConfig } from '@/utils/config'

describe('Config Utils', () => {
  it('uses default config', () => {
    const config = getConfig()
    expect(config.apiUrl).toBe('http://localhost:3000')
  })
  
  it('uses environment variables', () => {
    // 设置环境变量
    process.env.VITE_API_URL = 'https://api.example.com'
    
    const config = getConfig()
    expect(config.apiUrl).toBe('https://api.example.com')
    
    // 恢复环境变量
    delete process.env.VITE_API_URL
  })
})

10. 测试组织和过滤

使用测试组织和过滤功能:

// tests/components/ComplexComponent.spec.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import ComplexComponent from '@/components/ComplexComponent.vue'

describe('ComplexComponent', () => {
  // 测试套件级别的设置
  beforeAll(() => {
    console.log('Before all tests')
  })
  
  afterAll(() => {
    console.log('After all tests')
  })
  
  describe('Feature A', () => {
    it('should work correctly', () => {
      // 测试代码
    })
  })
  
  describe('Feature B', () => {
    it('should work correctly', () => {
      // 测试代码
    })
  })
  
  // 使用 only 只运行特定测试
  it.only('should be the only test run', () => {
    // 只有这个测试会被运行
  })
  
  // 使用 skip 跳过特定测试
  it.skip('should be skipped', () => {
    // 这个测试会被跳过
  })
})

11. 性能测试

使用 Vitest 进行性能测试:

// tests/utils/performance.spec.ts
import { describe, it, expect, bench } from 'vitest'
import { heavyComputation } from '@/utils/performance'

describe('Performance Tests', () => {
  it('computes result correctly', () => {
    const result = heavyComputation(1000)
    expect(result).toBeGreaterThan(0)
  })
  
  // 性能基准测试
  bench('heavyComputation with 1000 items', () => {
    heavyComputation(1000)
  })
  
  bench('heavyComputation with 2000 items', () => {
    heavyComputation(2000)
  })
})

最佳实践

1. 测试命名规范

  • 测试文件名:ComponentName.spec.tsuseFeature.spec.ts
  • 测试套件:使用 describe 组织相关测试
  • 测试用例:使用清晰、描述性的名称
  • 使用 ittest 定义测试用例

2. 测试隔离原则

  • 每个测试应该独立运行,不依赖其他测试的状态
  • 使用 beforeEachafterEach 管理测试状态
  • 避免共享可变状态

3. 测试金字塔

  • 单元测试:最多,测试单个功能或组件
  • 集成测试:次之,测试组件间的交互
  • E2E 测试:最少,测试完整的用户流程

4. Mock 最佳实践

  • 只 Mock 外部依赖,不 Mock 内部代码
  • 使用 vi.mock 进行模块级 Mock
  • 避免过度 Mock,保持测试的真实性
  • 确保 Mock 与实际实现一致

5. 断言最佳实践

  • 使用清晰、具体的断言
  • 每个测试只测试一个功能点
  • 使用 expect.assertions() 确保预期的断言数量
  • 避免模糊的断言,如 expect(true).toBe(true)

6. 测试覆盖率

  • 追求有意义的覆盖率,而不是 100% 的数字
  • 关注核心业务逻辑的覆盖率
  • 避免为了覆盖率而写无意义的测试
  • 定期审查覆盖率报告,识别未测试的代码

7. 持续集成

  • 在 CI 中运行测试
  • 配置测试覆盖率阈值
  • 集成测试结果到代码审查流程
  • 使用自动化测试确保代码质量

常见问题和解决方案

1. 组件渲染问题

问题:Vue 组件在测试中无法正确渲染

解决方案

  • 确保使用 @testing-library/vue@vue/test-utils
  • 检查组件的依赖是否正确 Mock
  • 确保测试环境配置正确(jsdom)

2. Mock 问题

问题:Mock 不生效或行为不符合预期

解决方案

  • 检查 Mock 定义的位置和顺序
  • 使用 vi.mock 而不是 jest.mock
  • 确保 Mock 的函数签名与实际实现一致
  • 使用 mockImplementation 定义复杂的 Mock 行为

3. 异步测试问题

问题:异步测试不稳定或超时

解决方案

  • 使用 async/await 处理异步操作
  • 使用 flushPromises 等待所有 Promise 完成
  • 合理设置测试超时时间
  • 避免在测试中使用真实的网络请求

4. 覆盖率问题

问题:测试覆盖率低或不准确

解决方案

  • 检查 coverage.includecoverage.exclude 配置
  • 确保使用正确的覆盖率提供商(istanbul 或 v8)
  • 避免测试生成的代码(如 Vue SFC 编译后的代码)
  • 审查覆盖率报告,识别未测试的代码路径

5. 测试速度问题

问题:测试套件运行缓慢

解决方案

  • 减少单个测试文件的测试数量
  • 避免在测试中使用真实的数据库或网络请求
  • 使用 test.concurrent 运行并发测试
  • 优化 Mock 实现,避免不必要的计算

进阶学习资源

  1. 官方文档

  2. 测试模式

  3. 工具和库

  4. 学习案例

实践练习

练习 1:基础测试套件

  1. 初始化一个 Vue 3 + Vitest 项目
  2. 配置 Vitest 和测试环境
  3. 实现一个简单的组件和组合式函数
  4. 为它们编写完整的测试套件

练习 2:组件测试高级技巧

  1. 实现一个复杂的表单组件
  2. 测试表单验证逻辑
  3. 测试异步提交功能
  4. 实现快照测试

练习 3:Mock 和集成测试

  1. 创建一个依赖外部 API 的组件
  2. Mock 外部 API 调用
  3. 测试组件的各种状态(加载、成功、错误)
  4. 测试组件间的交互

练习 4:性能测试和覆盖率优化

  1. 实现一个性能密集型的功能
  2. 编写性能基准测试
  3. 优化代码并比较性能差异
  4. 提高测试覆盖率到 80% 以上

总结

Vitest 是 Vue 3 生态系统中强大的测试框架,提供了丰富的功能和高效的测试体验。通过掌握 Vitest 的高级特性和最佳实践,你可以构建全面、可靠的测试套件,确保你的 Vue 3 应用质量。从单元测试到性能测试,从 Mock 到覆盖率,Vitest 提供了完整的测试解决方案,帮助你在开发过程中及早发现和修复问题。

下一集我们将学习 Vue 3 与 Playwright 高级 E2E 测试,敬请期待!

« 上一篇 Vue 3与Rollup库开发 - 高效模块打包全栈解决方案 下一篇 » Vue 3与Playwright高级E2E测试 - 现代化端到端测试全栈解决方案