feat: 添加测试框架和覆盖率报告功能
feat(测试): 新增Playwright和Vitest测试配置 feat(测试): 添加测试覆盖率报告生成功能 feat(测试): 实现前后端测试脚本集成 fix(测试): 修复测试密码不匹配问题 fix(测试): 修正URL等待策略 fix(测试): 调整错误消息选择器 refactor(测试): 重构测试目录结构 refactor(测试): 优化测试用例组织方式 docs: 更新测试报告文档 docs: 添加测试覆盖率报告模板 ci: 添加Docker测试环境配置 ci: 实现测试自动化脚本 chore: 更新依赖版本 chore: 添加测试相关配置文件
This commit is contained in:
@@ -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
|
||||
**维护者:** 张翔(全栈质量保障与研发效能工程师)
|
||||
Reference in New Issue
Block a user