Files
jiebanke/docs/测试文档.md

1581 lines
43 KiB
Markdown
Raw Normal View History

# 结伴客项目测试文档
## 1. 测试概述
### 1.1 测试目标
确保结伴客项目各个模块的功能正确性、性能稳定性、安全可靠性,为产品上线提供质量保障。
### 1.2 测试范围
- **后端API服务**:接口功能、性能、安全测试
- **小程序应用**:功能、兼容性、用户体验测试
- **管理后台**:功能、权限、数据一致性测试
- **数据库**:数据完整性、性能、备份恢复测试
- **系统集成**:各模块间集成测试
### 1.3 测试策略
```mermaid
graph TD
A[测试策略] --> B[单元测试]
A --> C[集成测试]
A --> D[系统测试]
A --> E[验收测试]
B --> B1[代码覆盖率>80%]
B --> B2[自动化执行]
C --> C1[API集成测试]
C --> C2[数据库集成测试]
D --> D1[功能测试]
D --> D2[性能测试]
D --> D3[安全测试]
E --> E1[用户验收测试]
E --> E2[业务流程验证]
## 2. 测试环境
### 2.1 环境配置
| 环境类型 | 用途 | 配置 | 数据库 |
|---------|------|------|--------|
| 开发环境 | 开发调试 | 本地Docker | MySQL 8.0 |
| 测试环境 | 功能测试 | 测试服务器 | MySQL 8.0 |
| 预发布环境 | 集成测试 | 生产同配置 | MySQL 8.0 |
| 生产环境 | 线上服务 | 高可用集群 | MySQL 8.0主从 |
### 2.2 测试数据管理
```yaml
# 测试数据配置
test_data:
users:
- username: "test_user_001"
email: "test001@example.com"
role: "user"
- username: "test_admin_001"
email: "admin001@example.com"
role: "admin"
trips:
- title: "测试旅行001"
destination: "北京"
start_date: "2024-06-01"
end_date: "2024-06-07"
animals:
- name: "测试动物001"
type: "cat"
location: "北京动物园"
```
## 3. 测试计划
### 3.1 测试阶段
```mermaid
gantt
title 测试计划时间线
dateFormat YYYY-MM-DD
section 单元测试
后端单元测试 :ut1, 2024-03-01, 7d
前端单元测试 :ut2, 2024-03-01, 7d
section 集成测试
API集成测试 :it1, after ut1, 5d
数据库集成测试 :it2, after ut1, 3d
section 系统测试
功能测试 :st1, after it1, 10d
性能测试 :st2, after it1, 7d
安全测试 :st3, after it1, 5d
section 验收测试
用户验收测试 :at1, after st1, 7d
```
### 3.2 测试用例设计
#### 3.2.1 后端API测试用例
```javascript
// 用户注册接口测试用例
describe('用户注册API', () => {
test('正常注册流程', async () => {
const userData = {
username: 'testuser001',
email: 'test@example.com',
password: 'Test123456',
phone: '13800138000'
};
const response = await request(app)
.post('/api/auth/register')
.send(userData)
.expect(200);
expect(response.body.code).toBe(0);
expect(response.body.data.user.username).toBe(userData.username);
});
test('重复邮箱注册', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
username: 'testuser002',
email: 'test@example.com', // 重复邮箱
password: 'Test123456'
})
.expect(400);
expect(response.body.code).toBe(40001);
expect(response.body.message).toContain('邮箱已存在');
});
});
```
#### 3.2.2 小程序功能测试用例
```javascript
// 小程序页面测试
describe('旅行结伴页面', () => {
test('页面正常加载', async () => {
const page = await miniProgram.reLaunch('/pages/trip/list');
await page.waitFor(2000);
const title = await page.$('.page-title');
expect(await title.text()).toBe('旅行结伴');
});
test('创建旅行结伴', async () => {
const page = await miniProgram.navigateTo('/pages/trip/create');
await page.setData({
'form.title': '测试旅行',
'form.destination': '北京',
'form.startDate': '2024-06-01',
'form.endDate': '2024-06-07'
});
await page.tap('.submit-btn');
await page.waitFor(1000);
expect(page.path).toBe('/pages/trip/detail');
});
});
```
## 4. 性能测试
### 4.1 性能指标
| 指标类型 | 目标值 | 测试方法 |
|---------|--------|----------|
| 接口响应时间 | < 500ms | JMeter压测 |
| 页面加载时间 | < 2s | Lighthouse |
| 并发用户数 | 1000+ | 压力测试 |
| 数据库查询 | < 100ms | SQL性能分析 |
| 内存使用率 | < 80% | 系统监控 |
### 4.2 JMeter压测脚本
```xml
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="结伴客API压测">
<elementProp name="TestPlan.arguments" elementType="Arguments" guiclass="ArgumentsPanel">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="用户登录压测">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">100</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">50</stringProp>
<stringProp name="ThreadGroup.ramp_time">10</stringProp>
</ThreadGroup>
</hashTree>
</hashTree>
</jmeterTestPlan>
```
### 4.3 Artillery性能测试
```yaml
# artillery-config.yml
config:
target: 'http://localhost:3000'
phases:
- duration: 60
arrivalRate: 10
- duration: 120
arrivalRate: 50
- duration: 60
arrivalRate: 100
scenarios:
- name: "用户注册登录流程"
flow:
- post:
url: "/api/auth/register"
json:
username: "test_{{ $randomString() }}"
email: "{{ $randomString() }}@test.com"
password: "Test123456"
- post:
url: "/api/auth/login"
json:
email: "{{ email }}"
password: "Test123456"
```
## 5. 安全测试
### 5.1 安全测试范围
- **身份认证安全**JWT令牌、密码加密
- **授权控制**:角色权限、接口鉴权
- **数据安全**SQL注入、XSS攻击
- **传输安全**HTTPS、数据加密
- **系统安全**:文件上传、敏感信息泄露
### 5.2 OWASP ZAP安全扫描
```yaml
# zap-baseline-scan.yml
version: '3'
services:
zap:
image: owasp/zap2docker-stable
command: zap-baseline.py -t http://host.docker.internal:3000 -r zap-report.html
volumes:
- ./reports:/zap/wrk/:rw
```
### 5.3 安全测试用例
```javascript
// SQL注入测试
describe('SQL注入防护测试', () => {
test('用户名SQL注入', async () => {
const maliciousInput = "admin'; DROP TABLE users; --";
const response = await request(app)
.post('/api/auth/login')
.send({
username: maliciousInput,
password: 'password'
});
// 应该返回错误而不是执行SQL
expect(response.status).toBe(400);
expect(response.body.message).toContain('参数格式错误');
});
});
// XSS攻击测试
describe('XSS防护测试', () => {
test('评论内容XSS过滤', async () => {
const xssPayload = '<script>alert("XSS")</script>';
const response = await request(app)
.post('/api/comments')
.set('Authorization', `Bearer ${token}`)
.send({
content: xssPayload,
tripId: 1
});
expect(response.body.data.content).not.toContain('<script>');
});
});
```
## 6. 测试报告
### 6.1 测试报告模板
```markdown
# 结伴客项目测试报告
## 测试概要
- **测试版本**: v1.0.0
- **测试时间**: 2024-03-01 ~ 2024-03-15
- **测试环境**: 测试环境
- **测试人员**: 测试团队
## 测试结果统计
- **总用例数**: 500
- **通过用例**: 485
- **失败用例**: 15
- **通过率**: 97%
## 缺陷统计
- **严重缺陷**: 0
- **一般缺陷**: 8
- **轻微缺陷**: 7
- **建议优化**: 12
## 性能测试结果
- **平均响应时间**: 245ms
- **最大并发用户**: 1200
- **系统稳定性**: 99.8%
## 安全测试结果
- **高危漏洞**: 0
- **中危漏洞**: 2
- **低危漏洞**: 5
## 测试结论
系统整体质量良好,满足上线要求。
```
### 6.2 自动化测试报告
```javascript
// jest.config.js
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
reporters: [
'default',
['jest-html-reporters', {
publicPath: './test-reports',
filename: 'test-report.html'
}]
]
};
```
## 7. 总结
### 7.1 测试策略总结
- **全面覆盖**:从单元测试到端到端测试的完整覆盖
- **自动化优先**80%以上测试用例实现自动化
- **持续集成**集成到CI/CD流程中
- **质量保证**:建立完善的质量门禁机制
### 7.2 测试工具链
- **单元测试**Jest, Vitest
- **集成测试**Supertest, TestContainers
- **E2E测试**Playwright, Cypress
- **性能测试**JMeter, Artillery
- **安全测试**OWASP ZAP, SonarQube
- **测试管理**TestRail, Jira
### 7.3 持续改进
- 定期回顾测试策略和流程
- 持续优化测试用例和自动化脚本
- 加强团队测试技能培训
- 建立测试最佳实践知识库
#### 4. 性能测试 (Performance Testing)
- **目标**: 验证系统性能指标
- **工具**: JMeter, K6, Artillery
- **覆盖**: 负载、压力、并发
## 🧪 测试策略
### 测试类型
#### 功能测试
```javascript
// 示例:用户登录功能测试
describe('用户登录功能', () => {
test('正确的用户名和密码应该登录成功', async () => {
const loginData = {
username: 'testuser',
password: 'password123'
}
const response = await request(app)
.post('/api/auth/login')
.send(loginData)
.expect(200)
expect(response.body).toHaveProperty('token')
expect(response.body.user.username).toBe('testuser')
})
test('错误的密码应该返回401错误', async () => {
const loginData = {
username: 'testuser',
password: 'wrongpassword'
}
const response = await request(app)
.post('/api/auth/login')
.send(loginData)
.expect(401)
expect(response.body.message).toBe('用户名或密码错误')
})
})
```
#### 安全测试
```javascript
// 示例SQL注入防护测试
describe('安全测试', () => {
test('应该防止SQL注入攻击', async () => {
const maliciousInput = "'; DROP TABLE users; --"
const response = await request(app)
.get(`/api/animals/search?name=${maliciousInput}`)
.expect(400)
// 验证数据库表仍然存在
const userCount = await User.count()
expect(userCount).toBeGreaterThan(0)
})
test('应该防止XSS攻击', async () => {
const xssPayload = '<script>alert("xss")</script>'
const response = await request(app)
.post('/api/animals')
.send({ name: xssPayload, description: 'test' })
.expect(400)
expect(response.body.message).toContain('输入包含非法字符')
})
})
```
#### 性能测试
```javascript
// 示例API响应时间测试
describe('性能测试', () => {
test('动物列表API响应时间应小于2秒', async () => {
const startTime = Date.now()
const response = await request(app)
.get('/api/animals')
.expect(200)
const responseTime = Date.now() - startTime
expect(responseTime).toBeLessThan(2000)
})
test('并发请求处理能力', async () => {
const promises = Array.from({ length: 100 }, () =>
request(app).get('/api/animals').expect(200)
)
const startTime = Date.now()
await Promise.all(promises)
const totalTime = Date.now() - startTime
expect(totalTime).toBeLessThan(10000) // 100个并发请求在10秒内完成
})
})
```
## 📝 测试用例设计
### 用户认证模块测试用例
#### 用户注册测试
```gherkin
Feature: 用户注册
作为一个新用户
我想要注册账户
以便使用系统功能
Scenario: 成功注册新用户
Given 我在注册页面
When 我输入有效的用户信息
| 字段 | 值 |
| 用户名 | testuser123 |
| 邮箱 | test@example.com |
| 密码 | Password123! |
| 确认密码 | Password123! |
And 我点击注册按钮
Then 我应该看到注册成功消息
And 我应该收到验证邮件
Scenario: 用户名已存在
Given 系统中已存在用户名为"existinguser"的用户
When 我尝试注册用户名为"existinguser"的账户
Then 我应该看到"用户名已存在"的错误消息
Scenario: 密码强度不足
Given 我在注册页面
When 我输入弱密码"123456"
Then 我应该看到密码强度提示
And 注册按钮应该被禁用
```
#### 动物管理测试用例
```gherkin
Feature: 动物信息管理
作为管理员
我想要管理动物信息
以便为用户提供准确的动物数据
Scenario: 添加新动物
Given 我以管理员身份登录
When 我填写动物信息表单
| 字段 | 值 |
| 名称 | 小白 |
| 物种 | 狗 |
| 年龄 | 2岁 |
| 性别 | 雌性 |
| 描述 | 温顺可爱的小狗 |
And 我上传动物照片
And 我点击保存按钮
Then 动物信息应该被成功保存
And 我应该在动物列表中看到新添加的动物
Scenario: 搜索动物
Given 系统中有多个动物记录
When 我在搜索框中输入"小白"
And 我点击搜索按钮
Then 我应该看到包含"小白"的搜索结果
And 结果应该按相关性排序
```
### API接口测试用例
#### 动物API测试
```javascript
describe('动物API测试', () => {
let authToken
let testAnimalId
beforeAll(async () => {
// 获取认证token
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ username: 'admin', password: 'admin123' })
authToken = loginResponse.body.token
})
describe('GET /api/animals', () => {
test('应该返回动物列表', async () => {
const response = await request(app)
.get('/api/animals')
.expect(200)
expect(response.body).toHaveProperty('data')
expect(response.body).toHaveProperty('total')
expect(response.body).toHaveProperty('page')
expect(Array.isArray(response.body.data)).toBe(true)
})
test('应该支持分页参数', async () => {
const response = await request(app)
.get('/api/animals?page=1&limit=5')
.expect(200)
expect(response.body.data.length).toBeLessThanOrEqual(5)
expect(response.body.page).toBe(1)
})
test('应该支持搜索功能', async () => {
const response = await request(app)
.get('/api/animals?search=狗')
.expect(200)
response.body.data.forEach(animal => {
expect(
animal.name.includes('狗') ||
animal.species.includes('狗') ||
animal.description.includes('狗')
).toBe(true)
})
})
})
describe('POST /api/animals', () => {
test('管理员应该能够创建动物', async () => {
const animalData = {
name: '测试动物',
species: '狗',
breed: '金毛',
gender: 'male',
age_months: 24,
description: '测试用动物'
}
const response = await request(app)
.post('/api/animals')
.set('Authorization', `Bearer ${authToken}`)
.send(animalData)
.expect(201)
expect(response.body.data).toHaveProperty('id')
expect(response.body.data.name).toBe(animalData.name)
testAnimalId = response.body.data.id
})
test('未认证用户不能创建动物', async () => {
const animalData = {
name: '测试动物',
species: '狗'
}
await request(app)
.post('/api/animals')
.send(animalData)
.expect(401)
})
test('应该验证必填字段', async () => {
const response = await request(app)
.post('/api/animals')
.set('Authorization', `Bearer ${authToken}`)
.send({})
.expect(400)
expect(response.body.errors).toContain('name is required')
expect(response.body.errors).toContain('species is required')
})
})
describe('PUT /api/animals/:id', () => {
test('应该能够更新动物信息', async () => {
const updateData = {
name: '更新后的名称',
description: '更新后的描述'
}
const response = await request(app)
.put(`/api/animals/${testAnimalId}`)
.set('Authorization', `Bearer ${authToken}`)
.send(updateData)
.expect(200)
expect(response.body.data.name).toBe(updateData.name)
expect(response.body.data.description).toBe(updateData.description)
})
test('更新不存在的动物应该返回404', async () => {
await request(app)
.put('/api/animals/999999')
.set('Authorization', `Bearer ${authToken}`)
.send({ name: '测试' })
.expect(404)
})
})
describe('DELETE /api/animals/:id', () => {
test('应该能够删除动物', async () => {
await request(app)
.delete(`/api/animals/${testAnimalId}`)
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
// 验证动物已被删除
await request(app)
.get(`/api/animals/${testAnimalId}`)
.expect(404)
})
})
})
```
## 🎭 前端测试
### 组件测试
#### Vue组件测试
```javascript
// AnimalCard.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import AnimalCard from '@/components/AnimalCard.vue'
describe('AnimalCard组件', () => {
const mockAnimal = {
id: 1,
name: '小白',
species: '狗',
age_months: 24,
gender: 'female',
description: '可爱的小狗',
images: ['https://example.com/image1.jpg'],
status: 'available'
}
it('应该正确渲染动物信息', () => {
const wrapper = mount(AnimalCard, {
props: { animal: mockAnimal }
})
expect(wrapper.find('.animal-name').text()).toBe('小白')
expect(wrapper.find('.animal-species').text()).toBe('狗')
expect(wrapper.find('.animal-age').text()).toContain('2岁')
expect(wrapper.find('.animal-description').text()).toBe('可爱的小狗')
})
it('应该显示动物图片', () => {
const wrapper = mount(AnimalCard, {
props: { animal: mockAnimal }
})
const img = wrapper.find('.animal-image')
expect(img.exists()).toBe(true)
expect(img.attributes('src')).toBe(mockAnimal.images[0])
expect(img.attributes('alt')).toBe(mockAnimal.name)
})
it('点击认领按钮应该触发事件', async () => {
const wrapper = mount(AnimalCard, {
props: { animal: mockAnimal }
})
const adoptButton = wrapper.find('.adopt-button')
await adoptButton.trigger('click')
expect(wrapper.emitted('adopt')).toBeTruthy()
expect(wrapper.emitted('adopt')[0]).toEqual([mockAnimal.id])
})
it('已认领的动物不应该显示认领按钮', () => {
const adoptedAnimal = { ...mockAnimal, status: 'adopted' }
const wrapper = mount(AnimalCard, {
props: { animal: adoptedAnimal }
})
expect(wrapper.find('.adopt-button').exists()).toBe(false)
expect(wrapper.find('.adopted-label').exists()).toBe(true)
})
it('应该处理图片加载错误', async () => {
const wrapper = mount(AnimalCard, {
props: { animal: mockAnimal }
})
const img = wrapper.find('.animal-image')
await img.trigger('error')
expect(wrapper.find('.image-placeholder').exists()).toBe(true)
})
})
```
#### 页面测试
```javascript
// AnimalList.test.js
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import AnimalList from '@/pages/AnimalList.vue'
import { useAnimalStore } from '@/stores/animal'
// Mock API
vi.mock('@/api/animal', () => ({
getAnimals: vi.fn(() => Promise.resolve({
data: [
{ id: 1, name: '小白', species: '狗' },
{ id: 2, name: '小黑', species: '猫' }
],
total: 2,
page: 1
}))
}))
describe('AnimalList页面', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('应该加载并显示动物列表', async () => {
const wrapper = mount(AnimalList)
// 等待异步加载完成
await wrapper.vm.$nextTick()
await new Promise(resolve => setTimeout(resolve, 100))
expect(wrapper.findAll('.animal-card')).toHaveLength(2)
expect(wrapper.find('.animal-card').text()).toContain('小白')
})
it('应该支持搜索功能', async () => {
const wrapper = mount(AnimalList)
const searchInput = wrapper.find('.search-input')
await searchInput.setValue('小白')
await searchInput.trigger('input')
// 验证搜索参数被传递
const animalStore = useAnimalStore()
expect(animalStore.searchParams.keyword).toBe('小白')
})
it('应该支持筛选功能', async () => {
const wrapper = mount(AnimalList)
const speciesFilter = wrapper.find('.species-filter')
await speciesFilter.setValue('狗')
await speciesFilter.trigger('change')
const animalStore = useAnimalStore()
expect(animalStore.searchParams.species).toBe('狗')
})
it('应该处理加载状态', () => {
const wrapper = mount(AnimalList)
// 模拟加载状态
const animalStore = useAnimalStore()
animalStore.loading = true
expect(wrapper.find('.loading-spinner').exists()).toBe(true)
})
it('应该处理错误状态', async () => {
// Mock API错误
vi.mocked(getAnimals).mockRejectedValueOnce(new Error('网络错误'))
const wrapper = mount(AnimalList)
await wrapper.vm.$nextTick()
expect(wrapper.find('.error-message').exists()).toBe(true)
expect(wrapper.find('.error-message').text()).toContain('加载失败')
})
})
```
### E2E测试
#### Playwright E2E测试
```javascript
// e2e/animal-adoption.spec.js
import { test, expect } from '@playwright/test'
test.describe('动物认领流程', () => {
test.beforeEach(async ({ page }) => {
// 登录用户
await page.goto('/login')
await page.fill('[data-testid="username"]', 'testuser')
await page.fill('[data-testid="password"]', 'password123')
await page.click('[data-testid="login-button"]')
await expect(page).toHaveURL('/')
})
test('完整的动物认领流程', async ({ page }) => {
// 1. 浏览动物列表
await page.goto('/animals')
await expect(page.locator('.animal-card')).toHaveCount.greaterThan(0)
// 2. 搜索特定动物
await page.fill('[data-testid="search-input"]', '小白')
await page.click('[data-testid="search-button"]')
await expect(page.locator('.animal-card')).toContainText('小白')
// 3. 查看动物详情
await page.click('.animal-card:first-child')
await expect(page).toHaveURL(/\/animals\/\d+/)
await expect(page.locator('.animal-detail')).toBeVisible()
// 4. 申请认领
await page.click('[data-testid="adopt-button"]')
await expect(page).toHaveURL(/\/adoption\/apply/)
// 5. 填写认领申请表
await page.fill('[data-testid="applicant-name"]', '张三')
await page.fill('[data-testid="applicant-phone"]', '13800138000')
await page.fill('[data-testid="applicant-email"]', 'zhangsan@example.com')
await page.fill('[data-testid="applicant-address"]', '北京市朝阳区')
await page.selectOption('[data-testid="housing-type"]', 'apartment')
await page.fill('[data-testid="adoption-reason"]', '我很喜欢小动物,希望给它一个温暖的家')
// 6. 提交申请
await page.click('[data-testid="submit-application"]')
await expect(page.locator('.success-message')).toContainText('申请提交成功')
// 7. 查看申请状态
await page.goto('/user/adoptions')
await expect(page.locator('.adoption-application')).toContainText('审核中')
})
test('动物搜索和筛选功能', async ({ page }) => {
await page.goto('/animals')
// 测试搜索功能
await page.fill('[data-testid="search-input"]', '狗')
await page.click('[data-testid="search-button"]')
const animalCards = page.locator('.animal-card')
const count = await animalCards.count()
for (let i = 0; i < count; i++) {
const card = animalCards.nth(i)
const text = await card.textContent()
expect(text).toMatch(/狗|犬/)
}
// 测试筛选功能
await page.selectOption('[data-testid="species-filter"]', '猫')
await page.waitForLoadState('networkidle')
const catCards = page.locator('.animal-card')
const catCount = await catCards.count()
for (let i = 0; i < catCount; i++) {
const card = catCards.nth(i)
const text = await card.textContent()
expect(text).toContain('猫')
}
})
test('响应式设计测试', async ({ page }) => {
// 测试桌面端
await page.setViewportSize({ width: 1200, height: 800 })
await page.goto('/animals')
await expect(page.locator('.animal-grid')).toHaveClass(/grid-cols-3/)
// 测试平板端
await page.setViewportSize({ width: 768, height: 1024 })
await page.reload()
await expect(page.locator('.animal-grid')).toHaveClass(/grid-cols-2/)
// 测试移动端
await page.setViewportSize({ width: 375, height: 667 })
await page.reload()
await expect(page.locator('.animal-grid')).toHaveClass(/grid-cols-1/)
await expect(page.locator('.mobile-nav')).toBeVisible()
})
})
```
## 🚀 性能测试
### 负载测试
#### JMeter测试计划
```xml
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="结伴客性能测试">
<stringProp name="TestPlan.comments">结伴客系统性能测试计划</stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量">
<collectionProp name="Arguments.arguments">
<elementProp name="BASE_URL" elementType="Argument">
<stringProp name="Argument.name">BASE_URL</stringProp>
<stringProp name="Argument.value">http://localhost:3001</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</TestPlan>
<hashTree>
<!-- 动物列表API负载测试 -->
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="动物列表API负载测试">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">10</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">100</stringProp>
<stringProp name="ThreadGroup.ramp_time">60</stringProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="获取动物列表">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="page" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">1</stringProp>
<stringProp name="Argument.name">page</stringProp>
</elementProp>
<elementProp name="limit" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">20</stringProp>
<stringProp name="Argument.name">limit</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain">${BASE_URL}</stringProp>
<stringProp name="HTTPSampler.path">/api/animals</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<!-- 响应时间断言 -->
<DurationAssertion guiclass="DurationAssertionGui" testclass="DurationAssertion" testname="响应时间断言">
<stringProp name="DurationAssertion.duration">2000</stringProp>
</DurationAssertion>
<!-- 响应状态断言 -->
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="响应状态断言">
<collectionProp name="Asserion.test_strings">
<stringProp>200</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">1</intProp>
</ResponseAssertion>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
```
#### K6性能测试脚本
```javascript
// performance/load-test.js
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Rate } from 'k6/metrics'
// 自定义指标
const errorRate = new Rate('errors')
// 测试配置
export const options = {
stages: [
{ duration: '2m', target: 10 }, // 预热阶段
{ duration: '5m', target: 50 }, // 负载增加
{ duration: '10m', target: 100 }, // 稳定负载
{ duration: '5m', target: 200 }, // 峰值负载
{ duration: '2m', target: 0 }, // 负载下降
],
thresholds: {
http_req_duration: ['p(95)<2000'], // 95%的请求响应时间小于2秒
http_req_failed: ['rate<0.1'], // 错误率小于10%
errors: ['rate<0.1'], // 自定义错误率小于10%
},
}
const BASE_URL = 'http://localhost:3001'
export default function () {
// 测试动物列表API
const animalsResponse = http.get(`${BASE_URL}/api/animals?page=1&limit=20`)
const animalsCheck = check(animalsResponse, {
'动物列表状态码为200': (r) => r.status === 200,
'动物列表响应时间<2s': (r) => r.timings.duration < 2000,
'动物列表包含数据': (r) => JSON.parse(r.body).data.length > 0,
})
errorRate.add(!animalsCheck)
// 测试动物详情API
if (animalsCheck) {
const animals = JSON.parse(animalsResponse.body).data
if (animals.length > 0) {
const randomAnimal = animals[Math.floor(Math.random() * animals.length)]
const detailResponse = http.get(`${BASE_URL}/api/animals/${randomAnimal.id}`)
const detailCheck = check(detailResponse, {
'动物详情状态码为200': (r) => r.status === 200,
'动物详情响应时间<1s': (r) => r.timings.duration < 1000,
'动物详情包含ID': (r) => JSON.parse(r.body).data.id === randomAnimal.id,
})
errorRate.add(!detailCheck)
}
}
// 测试搜索API
const searchResponse = http.get(`${BASE_URL}/api/animals?search=狗&page=1&limit=10`)
const searchCheck = check(searchResponse, {
'搜索状态码为200': (r) => r.status === 200,
'搜索响应时间<3s': (r) => r.timings.duration < 3000,
})
errorRate.add(!searchCheck)
sleep(1) // 模拟用户思考时间
}
// 测试完成后的处理
export function handleSummary(data) {
return {
'performance-report.html': htmlReport(data),
'performance-summary.json': JSON.stringify(data),
}
}
function htmlReport(data) {
return `
<!DOCTYPE html>
<html>
<head>
<title>性能测试报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.metric { margin: 10px 0; padding: 10px; border: 1px solid #ddd; }
.pass { background-color: #d4edda; }
.fail { background-color: #f8d7da; }
</style>
</head>
<body>
<h1>结伴客性能测试报告</h1>
<h2>测试概要</h2>
<div class="metric">
<strong>总请求数:</strong> ${data.metrics.http_reqs.count}
</div>
<div class="metric">
<strong>平均响应时间:</strong> ${data.metrics.http_req_duration.avg.toFixed(2)}ms
</div>
<div class="metric">
<strong>95%响应时间:</strong> ${data.metrics.http_req_duration['p(95)'].toFixed(2)}ms
</div>
<div class="metric ${data.metrics.http_req_failed.rate < 0.1 ? 'pass' : 'fail'}">
<strong>错误率:</strong> ${(data.metrics.http_req_failed.rate * 100).toFixed(2)}%
</div>
</body>
</html>
`
}
```
## 🔧 测试工具配置
### Jest配置
```javascript
// jest.config.js
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: [
'**/__tests__/**/*.js',
'**/?(*.)+(spec|test).js'
],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
'!src/config/**',
'!src/migrations/**'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
testTimeout: 10000,
verbose: true,
collectCoverage: true,
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
}
```
### Vitest配置
```javascript
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./tests/setup.js'],
coverage: {
provider: 'c8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.config.js'
]
}
},
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
})
```
### Playwright配置
```javascript
// playwright.config.js
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }]
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] }
}
],
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI
}
})
```
## 📊 测试报告
### 覆盖率报告
```javascript
// 生成覆盖率报告脚本
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
function generateCoverageReport() {
console.log('生成测试覆盖率报告...')
// 运行测试并生成覆盖率
execSync('npm run test:coverage', { stdio: 'inherit' })
// 读取覆盖率数据
const coverageFile = path.join(__dirname, 'coverage/coverage-summary.json')
const coverage = JSON.parse(fs.readFileSync(coverageFile, 'utf8'))
// 生成HTML报告
const htmlReport = generateHtmlReport(coverage)
fs.writeFileSync('coverage-report.html', htmlReport)
console.log('覆盖率报告已生成: coverage-report.html')
}
function generateHtmlReport(coverage) {
const total = coverage.total
return `
<!DOCTYPE html>
<html>
<head>
<title>测试覆盖率报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.summary { background: #f5f5f5; padding: 20px; border-radius: 5px; }
.metric { display: inline-block; margin: 10px; padding: 10px; border: 1px solid #ddd; }
.high { background-color: #d4edda; }
.medium { background-color: #fff3cd; }
.low { background-color: #f8d7da; }
</style>
</head>
<body>
<h1>结伴客测试覆盖率报告</h1>
<div class="summary">
<h2>总体覆盖率</h2>
<div class="metric ${getColorClass(total.lines.pct)}">
<strong>行覆盖率:</strong> ${total.lines.pct}%
</div>
<div class="metric ${getColorClass(total.functions.pct)}">
<strong>函数覆盖率:</strong> ${total.functions.pct}%
</div>
<div class="metric ${getColorClass(total.branches.pct)}">
<strong>分支覆盖率:</strong> ${total.branches.pct}%
</div>
<div class="metric ${getColorClass(total.statements.pct)}">
<strong>语句覆盖率:</strong> ${total.statements.pct}%
</div>
</div>
<h2>详细信息</h2>
<p>总行数: ${total.lines.total}</p>
<p>已覆盖行数: ${total.lines.covered}</p>
<p>未覆盖行数: ${total.lines.skipped}</p>
<h2>建议</h2>
<ul>
${total.lines.pct < 80 ? '<li>行覆盖率低于80%,需要增加测试用例</li>' : ''}
${total.functions.pct < 80 ? '<li>函数覆盖率低于80%,需要测试更多函数</li>' : ''}
${total.branches.pct < 80 ? '<li>分支覆盖率低于80%,需要测试更多分支条件</li>' : ''}
</ul>
</body>
</html>
`
}
function getColorClass(percentage) {
if (percentage >= 80) return 'high'
if (percentage >= 60) return 'medium'
return 'low'
}
generateCoverageReport()
```
## 🔄 CI/CD集成
### GitHub Actions测试工作流
```yaml
# .github/workflows/test.yml
name: 测试工作流
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
unit-tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: jiebanke_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
redis:
image: redis:6
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v3
- name: 设置Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: 安装依赖
run: |
cd backend
npm ci
- name: 运行数据库迁移
run: |
cd backend
npm run migrate
env:
DB_HOST: localhost
DB_PORT: 3306
DB_NAME: jiebanke_test
DB_USER: root
DB_PASS: root
- name: 运行单元测试
run: |
cd backend
npm run test:coverage
env:
NODE_ENV: test
DB_HOST: localhost
DB_PORT: 3306
DB_NAME: jiebanke_test
DB_USER: root
DB_PASS: root
REDIS_HOST: localhost
REDIS_PORT: 6379
- name: 上传覆盖率报告
uses: codecov/codecov-action@v3
with:
file: ./backend/coverage/lcov.info
flags: backend
name: backend-coverage
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 设置Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: 安装依赖
run: |
cd frontend
npm ci
- name: 运行前端测试
run: |
cd frontend
npm run test:coverage
- name: 上传覆盖率报告
uses: codecov/codecov-action@v3
with:
file: ./frontend/coverage/lcov.info
flags: frontend
name: frontend-coverage
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 设置Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: 安装依赖
run: npm ci
- name: 安装Playwright
run: npx playwright install --with-deps
- name: 启动应用
run: |
npm run build
npm run start &
sleep 30
- name: 运行E2E测试
run: npx playwright test
- name: 上传测试报告
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
performance-tests:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: 设置Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: 安装K6
run: |
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: 启动应用
run: |
npm run build
npm run start &
sleep 30
- name: 运行性能测试
run: k6 run performance/load-test.js
- name: 上传性能报告
uses: actions/upload-artifact@v3
with:
name: performance-report
path: performance-report.html
```
## 📚 总结
本测试文档全面覆盖了结伴客项目的测试策略和实施方案,包括:
### 测试体系特点
1. **全面覆盖**: 从单元测试到E2E测试的完整测试金字塔
2. **自动化程度高**: CI/CD集成自动运行测试和生成报告
3. **质量保证**: 代码覆盖率要求和性能指标监控
4. **多维度测试**: 功能、性能、安全、兼容性全方位测试
### 关键测试工具
- **Jest/Vitest**: 单元测试和集成测试
- **Playwright**: 端到端测试
- **K6/JMeter**: 性能测试
- **GitHub Actions**: CI/CD自动化
### 质量指标
- 代码覆盖率 ≥ 80%
- API响应时间 < 2秒
- 系统可用性 99.9%
- 错误率 < 0.1%
### 持续改进
1. 定期审查和更新测试用例
2. 监控测试执行时间和稳定性
3. 根据业务变化调整测试策略
4. 培训团队成员测试最佳实践
通过完善的测试体系,确保结伴客项目的高质量交付和稳定运行。
---
**文档版本**: v1.0.0
**最后更新**: 2024年1月15日
**维护人员**: 测试团队