feat: 添加测试框架和覆盖率报告功能

feat(测试): 新增Playwright和Vitest测试配置
feat(测试): 添加测试覆盖率报告生成功能
feat(测试): 实现前后端测试脚本集成

fix(测试): 修复测试密码不匹配问题
fix(测试): 修正URL等待策略
fix(测试): 调整错误消息选择器

refactor(测试): 重构测试目录结构
refactor(测试): 优化测试用例组织方式

docs: 更新测试报告文档
docs: 添加测试覆盖率报告模板

ci: 添加Docker测试环境配置
ci: 实现测试自动化脚本

chore: 更新依赖版本
chore: 添加测试相关配置文件
This commit is contained in:
张翔
2026-03-25 09:03:37 +08:00
parent 117978e148
commit e2ad1331cc
126 changed files with 18083 additions and 7805 deletions
+456
View File
@@ -0,0 +1,456 @@
# 前端单元测试指南
## 📋 目录
- [测试环境配置](#测试环境配置)
- [测试工具函数](#测试工具函数)
- [测试数据](#测试数据)
- [编写单元测试](#编写单元测试)
- [测试最佳实践](#测试最佳实践)
---
## 测试环境配置
### 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
**维护者:** 张翔(全栈质量保障与研发效能工程师)