Files
novalon-manage-system/novalon-manage-web/UNIT_TEST_GUIDE.md
T
张翔 e2ad1331cc feat: 添加测试框架和覆盖率报告功能
feat(测试): 新增Playwright和Vitest测试配置
feat(测试): 添加测试覆盖率报告生成功能
feat(测试): 实现前后端测试脚本集成

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

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

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

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

chore: 更新依赖版本
chore: 添加测试相关配置文件
2026-03-25 09:03:37 +08:00

457 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端单元测试指南
## 📋 目录
- [测试环境配置](#测试环境配置)
- [测试工具函数](#测试工具函数)
- [测试数据](#测试数据)
- [编写单元测试](#编写单元测试)
- [测试最佳实践](#测试最佳实践)
---
## 测试环境配置
### 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
**维护者:** 张翔(全栈质量保障与研发效能工程师)