551 lines
13 KiB
Markdown
551 lines
13 KiB
Markdown
|
|
# 测试文档
|
||
|
|
|
||
|
|
## 测试概述
|
||
|
|
|
||
|
|
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 秒
|
||
|
|
- **安全测试**: 所有安全测试必须通过
|