refactor: 替换项目中的"yudao"为"AIOTAGRO",并清理相关配置文件
This commit is contained in:
551
docs/测试文档.md
Normal file
551
docs/测试文档.md
Normal file
@@ -0,0 +1,551 @@
|
||||
# 测试文档
|
||||
|
||||
## 测试概述
|
||||
|
||||
AIOTAGRO 管理系统采用全面的测试策略,确保系统质量和稳定性。测试覆盖单元测试、集成测试和端到端测试。
|
||||
|
||||
## 测试环境
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **Node.js**: 18.0.0+
|
||||
- **pnpm**: 8.0.0+
|
||||
- **浏览器**: Chrome 90+, Firefox 85+, Safari 14+
|
||||
|
||||
### 测试工具栈
|
||||
|
||||
| 工具 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| Vitest | 1.0.0+ | 单元测试框架 |
|
||||
| Vue Test Utils | 2.4.0+ | Vue 组件测试 |
|
||||
| Playwright | 1.40.0+ | E2E 测试 |
|
||||
| Testing Library | 6.0.0+ | 组件测试工具 |
|
||||
|
||||
## 单元测试
|
||||
|
||||
### 测试配置
|
||||
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['**/__tests__/**/*.spec.ts'],
|
||||
coverage: {
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'**/*.d.ts',
|
||||
'**/types/**'
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 组件测试示例
|
||||
|
||||
```typescript
|
||||
// __tests__/components/UserInfo.spec.ts
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserInfo from '@/components/UserInfo.vue'
|
||||
|
||||
describe('UserInfo', () => {
|
||||
it('renders user information correctly', () => {
|
||||
const user = {
|
||||
id: 1,
|
||||
name: '张三',
|
||||
email: 'zhangsan@example.com',
|
||||
avatar: '/avatar.jpg'
|
||||
}
|
||||
|
||||
const wrapper = mount(UserInfo, {
|
||||
props: { user }
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('张三')
|
||||
expect(wrapper.text()).toContain('zhangsan@example.com')
|
||||
expect(wrapper.find('img').attributes('src')).toBe('/avatar.jpg')
|
||||
})
|
||||
|
||||
it('emits edit event when edit button is clicked', async () => {
|
||||
const user = {
|
||||
id: 1,
|
||||
name: '张三',
|
||||
email: 'zhangsan@example.com'
|
||||
}
|
||||
|
||||
const wrapper = mount(UserInfo, {
|
||||
props: { user }
|
||||
})
|
||||
|
||||
await wrapper.find('[data-testid="edit-btn"]').trigger('click')
|
||||
expect(wrapper.emitted('edit')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 工具函数测试
|
||||
|
||||
```typescript
|
||||
// __tests__/utils/format.spec.ts
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { formatDate, formatCurrency } from '@/utils/format'
|
||||
|
||||
describe('format utils', () => {
|
||||
describe('formatDate', () => {
|
||||
it('formats date correctly', () => {
|
||||
const date = new Date('2023-12-01')
|
||||
expect(formatDate(date)).toBe('2023-12-01')
|
||||
})
|
||||
|
||||
it('handles invalid date', () => {
|
||||
expect(formatDate(null)).toBe('')
|
||||
expect(formatDate(undefined)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatCurrency', () => {
|
||||
it('formats currency correctly', () => {
|
||||
expect(formatCurrency(1234.56)).toBe('¥1,234.56')
|
||||
expect(formatCurrency(0)).toBe('¥0.00')
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 集成测试
|
||||
|
||||
### API 集成测试
|
||||
|
||||
```typescript
|
||||
// __tests__/api/user.spec.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { userApi } from '@/api/user'
|
||||
import { mockServer } from '../mocks/server'
|
||||
|
||||
describe('User API', () => {
|
||||
beforeEach(() => {
|
||||
mockServer.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockServer.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
mockServer.close()
|
||||
})
|
||||
|
||||
it('fetches user list successfully', async () => {
|
||||
const response = await userApi.getUsers({ page: 1, size: 10 })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.data.list).toHaveLength(2)
|
||||
expect(response.data.total).toBe(2)
|
||||
})
|
||||
|
||||
it('handles API errors correctly', async () => {
|
||||
mockServer.use(
|
||||
rest.get('/api/users', (req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ message: 'Internal Server Error' }))
|
||||
})
|
||||
)
|
||||
|
||||
await expect(userApi.getUsers({ page: 1, size: 10 })).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 状态管理测试
|
||||
|
||||
```typescript
|
||||
// __tests__/stores/user.spec.ts
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
describe('User Store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('initializes with default values', () => {
|
||||
const store = useUserStore()
|
||||
|
||||
expect(store.user).toBeNull()
|
||||
expect(store.isLoggedIn).toBe(false)
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('sets user correctly', () => {
|
||||
const store = useUserStore()
|
||||
const user = { id: 1, name: '张三', email: 'zhangsan@example.com' }
|
||||
|
||||
store.setUser(user)
|
||||
|
||||
expect(store.user).toEqual(user)
|
||||
expect(store.isLoggedIn).toBe(true)
|
||||
})
|
||||
|
||||
it('clears user on logout', () => {
|
||||
const store = useUserStore()
|
||||
const user = { id: 1, name: '张三', email: 'zhangsan@example.com' }
|
||||
|
||||
store.setUser(user)
|
||||
store.logout()
|
||||
|
||||
expect(store.user).toBeNull()
|
||||
expect(store.isLoggedIn).toBe(false)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## E2E 测试
|
||||
|
||||
### 测试配置
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './__tests__/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'pnpm dev:antd',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 登录流程测试
|
||||
|
||||
```typescript
|
||||
// __tests__/e2e/login.spec.ts
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Login Flow', () => {
|
||||
test('should login successfully with valid credentials', async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
|
||||
// 填写登录表单
|
||||
await page.fill('[data-testid="username"]', 'admin')
|
||||
await page.fill('[data-testid="password"]', '123456')
|
||||
await page.click('[data-testid="login-btn"]')
|
||||
|
||||
// 验证登录成功
|
||||
await expect(page).toHaveURL('/dashboard')
|
||||
await expect(page.locator('[data-testid="user-name"]')).toContainText('管理员')
|
||||
})
|
||||
|
||||
test('should show error message with invalid credentials', async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
|
||||
// 填写错误凭证
|
||||
await page.fill('[data-testid="username"]', 'wronguser')
|
||||
await page.fill('[data-testid="password"]', 'wrongpass')
|
||||
await page.click('[data-testid="login-btn"]')
|
||||
|
||||
// 验证错误提示
|
||||
await expect(page.locator('[data-testid="error-message"]')).toBeVisible()
|
||||
await expect(page).toHaveURL('/login')
|
||||
})
|
||||
|
||||
test('should redirect to login when accessing protected page without authentication', async ({ page }) => {
|
||||
// 直接访问受保护页面
|
||||
await page.goto('/user-management')
|
||||
|
||||
// 验证重定向到登录页
|
||||
await expect(page).toHaveURL('/login')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 用户管理测试
|
||||
|
||||
```typescript
|
||||
// __tests__/e2e/user-management.spec.ts
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('User Management', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 登录
|
||||
await page.goto('/login')
|
||||
await page.fill('[data-testid="username"]', 'admin')
|
||||
await page.fill('[data-testid="password"]', '123456')
|
||||
await page.click('[data-testid="login-btn"]')
|
||||
await page.goto('/user-management')
|
||||
})
|
||||
|
||||
test('should display user list', async ({ page }) => {
|
||||
await expect(page.locator('[data-testid="user-table"]')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="user-row"]').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('should create new user', async ({ page }) => {
|
||||
// 点击新建按钮
|
||||
await page.click('[data-testid="create-user-btn"]')
|
||||
|
||||
// 填写用户信息
|
||||
await page.fill('[data-testid="user-name"]', '测试用户')
|
||||
await page.fill('[data-testid="user-email"]', 'test@example.com')
|
||||
await page.click('[data-testid="save-btn"]')
|
||||
|
||||
// 验证创建成功
|
||||
await expect(page.locator('[data-testid="success-message"]')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="user-table"]')).toContainText('测试用户')
|
||||
})
|
||||
|
||||
test('should delete user', async ({ page }) => {
|
||||
// 点击删除按钮
|
||||
await page.click('[data-testid="delete-user-btn"]').first()
|
||||
await page.click('[data-testid="confirm-delete-btn"]')
|
||||
|
||||
// 验证删除成功
|
||||
await expect(page.locator('[data-testid="success-message"]')).toBeVisible()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 性能测试
|
||||
|
||||
### 加载性能测试
|
||||
|
||||
```typescript
|
||||
// __tests__/performance/loading.spec.ts
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Performance Tests', () => {
|
||||
test('should load dashboard within 3 seconds', async ({ page }) => {
|
||||
const startTime = Date.now()
|
||||
await page.goto('/dashboard')
|
||||
const loadTime = Date.now() - startTime
|
||||
|
||||
expect(loadTime).toBeLessThan(3000)
|
||||
})
|
||||
|
||||
test('should render user table with 1000 rows efficiently', async ({ page }) => {
|
||||
await page.goto('/user-management')
|
||||
|
||||
// 模拟大量数据
|
||||
await page.evaluate(() => {
|
||||
window.performance.mark('table-render-start')
|
||||
})
|
||||
|
||||
// 等待表格渲染完成
|
||||
await page.waitForSelector('[data-testid="user-table"]')
|
||||
|
||||
const renderTime = await page.evaluate(() => {
|
||||
window.performance.mark('table-render-end')
|
||||
window.performance.measure('table-render', 'table-render-start', 'table-render-end')
|
||||
const measure = window.performance.getEntriesByName('table-render')[0]
|
||||
return measure.duration
|
||||
})
|
||||
|
||||
expect(renderTime).toBeLessThan(1000)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 安全测试
|
||||
|
||||
### XSS 防护测试
|
||||
|
||||
```typescript
|
||||
// __tests__/security/xss.spec.ts
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Security Tests', () => {
|
||||
test('should sanitize user input to prevent XSS', async ({ page }) => {
|
||||
await page.goto('/user-management')
|
||||
|
||||
// 尝试注入 XSS 代码
|
||||
const xssPayload = '<script>alert("XSS")</script>'
|
||||
await page.fill('[data-testid="user-name"]', xssPayload)
|
||||
await page.click('[data-testid="save-btn"]')
|
||||
|
||||
// 验证输入被正确转义
|
||||
const userNameCell = await page.locator('[data-testid="user-name"]').first()
|
||||
const innerHTML = await userNameCell.innerHTML()
|
||||
|
||||
expect(innerHTML).not.toContain('<script>')
|
||||
expect(innerHTML).toContain('<script>')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 测试覆盖率
|
||||
|
||||
### 覆盖率配置
|
||||
|
||||
```json
|
||||
{
|
||||
"coverage": {
|
||||
"provider": "v8",
|
||||
"reporter": ["text", "json", "html"],
|
||||
"reportsDirectory": "./coverage",
|
||||
"exclude": [
|
||||
"**/*.d.ts",
|
||||
"**/types/**",
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/coverage/**"
|
||||
],
|
||||
"thresholds": {
|
||||
"lines": 80,
|
||||
"functions": 80,
|
||||
"branches": 70,
|
||||
"statements": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 覆盖率报告
|
||||
|
||||
```bash
|
||||
# 生成覆盖率报告
|
||||
pnpm test:coverage
|
||||
|
||||
# 查看 HTML 报告
|
||||
open coverage/index.html
|
||||
```
|
||||
|
||||
## 测试执行
|
||||
|
||||
### 开发环境测试
|
||||
|
||||
```bash
|
||||
# 运行单元测试
|
||||
pnpm test:unit
|
||||
|
||||
# 运行 E2E 测试
|
||||
pnpm test:e2e
|
||||
|
||||
# 运行所有测试
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### CI/CD 环境测试
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Test
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm test:unit
|
||||
- run: pnpm test:e2e
|
||||
- run: pnpm test:coverage
|
||||
```
|
||||
|
||||
## 测试最佳实践
|
||||
|
||||
### 1. 测试命名规范
|
||||
- 描述性测试名称
|
||||
- 使用 Given-When-Then 模式
|
||||
- 避免模糊的测试描述
|
||||
|
||||
### 2. 测试数据管理
|
||||
- 使用测试工厂函数
|
||||
- 避免硬编码数据
|
||||
- 清理测试数据
|
||||
|
||||
### 3. 测试隔离
|
||||
- 每个测试独立运行
|
||||
- 避免测试间依赖
|
||||
- 使用 beforeEach/afterEach
|
||||
|
||||
### 4. 异步测试
|
||||
- 正确处理异步操作
|
||||
- 使用适当的等待策略
|
||||
- 避免不必要的等待
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 测试超时
|
||||
```typescript
|
||||
// 增加超时时间
|
||||
test('slow operation', async ({ page }) => {
|
||||
test.setTimeout(60000)
|
||||
// 测试代码
|
||||
})
|
||||
```
|
||||
|
||||
#### 2. 元素找不到
|
||||
```typescript
|
||||
// 使用更稳定的选择器
|
||||
await page.locator('[data-testid="submit-btn"]').click()
|
||||
```
|
||||
|
||||
#### 3. 网络请求失败
|
||||
```typescript
|
||||
// 等待网络请求完成
|
||||
await page.waitForResponse(response =>
|
||||
response.url().includes('/api/users') && response.status() === 200
|
||||
)
|
||||
```
|
||||
|
||||
## 测试报告
|
||||
|
||||
### 生成测试报告
|
||||
|
||||
```bash
|
||||
# 生成 JUnit 报告
|
||||
pnpm test:report
|
||||
|
||||
# 生成覆盖率报告
|
||||
pnpm test:coverage:report
|
||||
```
|
||||
|
||||
### 报告解读
|
||||
|
||||
- **测试通过率**: 应保持在 95% 以上
|
||||
- **代码覆盖率**: 单元测试覆盖率应达到 80% 以上
|
||||
- **性能指标**: 关键页面加载时间应小于 3 秒
|
||||
- **安全测试**: 所有安全测试必须通过
|
||||
Reference in New Issue
Block a user