13 KiB
13 KiB
测试文档
测试概述
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+ | 组件测试工具 |
单元测试
测试配置
// 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/**'
]
}
}
})
组件测试示例
// __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()
})
})
工具函数测试
// __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 集成测试
// __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()
})
})
状态管理测试
// __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 测试
测试配置
// 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,
},
})
登录流程测试
// __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')
})
})
用户管理测试
// __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()
})
})
性能测试
加载性能测试
// __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 防护测试
// __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>')
})
})
测试覆盖率
覆盖率配置
{
"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
}
}
}
覆盖率报告
# 生成覆盖率报告
pnpm test:coverage
# 查看 HTML 报告
open coverage/index.html
测试执行
开发环境测试
# 运行单元测试
pnpm test:unit
# 运行 E2E 测试
pnpm test:e2e
# 运行所有测试
pnpm test
CI/CD 环境测试
# .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. 测试超时
// 增加超时时间
test('slow operation', async ({ page }) => {
test.setTimeout(60000)
// 测试代码
})
2. 元素找不到
// 使用更稳定的选择器
await page.locator('[data-testid="submit-btn"]').click()
3. 网络请求失败
// 等待网络请求完成
await page.waitForResponse(response =>
response.url().includes('/api/users') && response.status() === 200
)
测试报告
生成测试报告
# 生成 JUnit 报告
pnpm test:report
# 生成覆盖率报告
pnpm test:coverage:report
报告解读
- 测试通过率: 应保持在 95% 以上
- 代码覆盖率: 单元测试覆盖率应达到 80% 以上
- 性能指标: 关键页面加载时间应小于 3 秒
- 安全测试: 所有安全测试必须通过