# 前端单元测试指南 ## 📋 目录 - [测试环境配置](#测试环境配置) - [测试工具函数](#测试工具函数) - [测试数据](#测试数据) - [编写单元测试](#编写单元测试) - [测试最佳实践](#测试最佳实践) --- ## 测试环境配置 ### Vitest 配置 项目使用 Vitest 作为单元测试框架,配置文件位于 `vitest.config.ts`: ```typescript import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import { fileURLToPath } from 'node:url' export default defineConfig({ plugins: [vue()], test: { globals: true, environment: 'jsdom', setupFiles: ['./src/test/setup.ts'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html', 'lcov'], exclude: [ 'node_modules/', 'src/test/', '**/*.d.ts', '**/*.config.*', '**/mockData', 'e2e/', ], lines: 80, functions: 80, branches: 80, statements: 80, }, }, resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, }) ``` ### 测试脚本 ```bash # 运行所有单元测试 npm test # 运行特定测试文件 npm test -- src/test/auth.test.ts # 运行测试并生成覆盖率报告 npm run test:coverage # 运行测试UI界面 npm run test:ui ``` --- ## 测试工具函数 ### createTestHelpers 创建测试辅助函数,简化组件测试: ```typescript import { createTestHelpers } from '@/test/utils' import { mount } from '@vue/test-utils' import MyComponent from '@/components/MyComponent.vue' const wrapper = mount(MyComponent) const helpers = createTestHelpers(wrapper) // 查找元素 const element = helpers.findByTestId('submit-button') // 点击元素 await helpers.clickByTestId('submit-button') // 填写表单 await helpers.fillByTestId('username', 'testuser') ``` ### waitFor 等待条件满足: ```typescript import { waitFor } from '@/test/utils' await waitFor(() => { return wrapper.text().includes('Success') }) ``` --- ## 测试数据 ### Mock 数据 使用 `src/test/fixtures.ts` 中的预定义 mock 数据: ```typescript import { mockUser, mockRole, mockApiResponse } from '@/test/fixtures' // 使用 mock 用户数据 const user = mockUser // 使用 mock API 响应 const response = mockApiResponse(user) ``` --- ## 编写单元测试 ### 基础测试示例 #### 1. 测试工具函数 ```typescript import { describe, it, expect } from 'vitest' import { formatDate } from '@/utils/date' describe('formatDate', () => { it('should format date correctly', () => { const date = new Date('2024-01-01') const result = formatDate(date) expect(result).toBe('2024-01-01') }) it('should handle null input', () => { const result = formatDate(null) expect(result).toBe('') }) }) ``` #### 2. 测试 Vue 组件 ```typescript import { describe, it, expect, beforeEach } from 'vitest' import { mount } from '@vue/test-utils' import Login from '@/views/system/Login.vue' describe('Login Component', () => { let wrapper: any beforeEach(() => { wrapper = mount(Login) }) it('should render login form', () => { expect(wrapper.find('input[type="text"]').exists()).toBe(true) expect(wrapper.find('input[type="password"]').exists()).toBe(true) expect(wrapper.find('button[type="submit"]').exists()).toBe(true) }) it('should update username input', async () => { const input = wrapper.find('input[type="text"]') await input.setValue('testuser') expect(input.element.value).toBe('testuser') }) it('should emit login event on form submit', async () => { const form = wrapper.find('form') await form.trigger('submit.prevent') expect(wrapper.emitted('login')).toBeTruthy() }) }) ``` #### 3. 测试 API 客户端 ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest' import { authApi } from '@/api/auth.api' import axios from 'axios' vi.mock('axios') describe('authApi', () => { beforeEach(() => { vi.clearAllMocks() }) it('should login successfully', async () => { const mockResponse = { data: { token: 'test-token', user: { id: 1, username: 'testuser' } } } vi.mocked(axios.post).mockResolvedValue(mockResponse) const result = await authApi.login({ username: 'testuser', password: 'password' }) expect(result.token).toBe('test-token') expect(result.user.username).toBe('testuser') expect(axios.post).toHaveBeenCalledWith('/auth/login', { username: 'testuser', password: 'password' }) }) }) ``` #### 4. 测试 Pinia Store ```typescript import { describe, it, expect, beforeEach } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { useUserStore } from '@/stores/user' describe('User Store', () => { beforeEach(() => { setActivePinia(createPinia()) }) it('should set user', () => { const store = useUserStore() const mockUser = { id: 1, username: 'testuser' } store.setUser(mockUser) expect(store.user).toEqual(mockUser) }) it('should clear user on logout', () => { const store = useUserStore() store.setUser({ id: 1, username: 'testuser' }) store.logout() expect(store.user).toBeNull() }) }) ``` --- ## 测试最佳实践 ### 1. 测试命名 使用清晰的测试名称,描述行为而非实现: ```typescript // ✅ 好的测试名称 it('should reject empty username') it('should display error message when login fails') // ❌ 不好的测试名称 it('test1') it('test login function') ``` ### 2. 测试隔离 每个测试应该独立,不依赖其他测试: ```typescript describe('User Component', () => { let wrapper: any beforeEach(() => { // 每个测试前重新创建组件 wrapper = mount(UserComponent) }) afterEach(() => { // 每个测试后清理 wrapper.unmount() }) }) ``` ### 3. 测试覆盖率 确保测试覆盖所有代码路径: ```typescript describe('validateEmail', () => { it('should accept valid email', () => { expect(validateEmail('test@example.com')).toBe(true) }) it('should reject empty email', () => { expect(validateEmail('')).toBe(false) }) it('should reject invalid email format', () => { expect(validateEmail('invalid')).toBe(false) }) }) ``` ### 4. Mock 外部依赖 使用 mock 隔离外部依赖: ```typescript import { vi } from 'vitest' // Mock API 调用 vi.mock('@/api/user.api', () => ({ userApi: { getUsers: vi.fn().mockResolvedValue([]) } })) // Mock 浏览器 API Object.defineProperty(window, 'localStorage', { value: { getItem: vi.fn(), setItem: vi.fn(), } }) ``` ### 5. 异步测试 正确处理异步操作: ```typescript it('should handle async operation', async () => { const wrapper = mount(Component) // 等待异步操作完成 await wrapper.vm.$nextTick() await waitFor(() => wrapper.text().includes('Loaded')) expect(wrapper.text()).toContain('Loaded') }) ``` ### 6. 测试用户交互 模拟用户交互: ```typescript it('should handle button click', async () => { const wrapper = mount(Component) const button = wrapper.find('button') await button.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.emitted('click')).toBeTruthy() }) it('should handle form submission', async () => { const wrapper = mount(Component) const form = wrapper.find('form') await form.trigger('submit.prevent') await wrapper.vm.$nextTick() expect(wrapper.emitted('submit')).toBeTruthy() }) ``` --- ## 运行测试 ### 运行所有测试 ```bash npm test ``` ### 运行特定测试文件 ```bash npm test -- src/test/auth.test.ts ``` ### 运行特定测试用例 ```bash npm test -- src/test/auth.test.ts -t "should login successfully" ``` ### 生成覆盖率报告 ```bash npm run test:coverage ``` 覆盖率报告将生成在 `coverage/` 目录下。 --- ## 常见问题 ### 1. 测试超时 增加测试超时时间: ```typescript it('should handle long operation', async () => { // 增加超时时间到 10 秒 }, 10000) ``` ### 2. Mock 不生效 确保 mock 在导入模块之前: ```typescript // ❌ 错误:在导入之后 mock import { authApi } from '@/api/auth.api' vi.mock('@/api/auth.api') // ✅ 正确:在导入之前 mock vi.mock('@/api/auth.api') import { authApi } from '@/api/auth.api' ``` ### 3. 组件渲染问题 使用 `shallowMount` 替代 `mount` 减少依赖: ```typescript import { shallowMount } from '@vue/test-utils' const wrapper = shallowMount(Component) ``` --- ## 参考资料 - [Vitest 官方文档](https://vitest.dev/) - [Vue Test Utils 官方文档](https://test-utils.vuejs.org/) - [Vue 3 测试指南](https://vuejs.org/guide/scaling-up/testing.html) --- **最后更新时间:** 2026-03-24 **维护者:** 张翔(全栈质量保障与研发效能工程师)