# 解班客测试文档
## 📋 概述
本文档详细描述解班客项目的测试策略、测试流程、测试用例设计和质量保证体系。通过全面的测试覆盖,确保系统的稳定性、可靠性和用户体验。
## 🎯 测试目标
### 主要目标
- **功能完整性**: 确保所有功能按需求正确实现
- **系统稳定性**: 保证系统在各种条件下稳定运行
- **性能达标**: 满足性能指标和用户体验要求
- **安全可靠**: 确保数据安全和系统安全
- **兼容性**: 支持多平台和多浏览器
### 质量标准
- **代码覆盖率**: ≥ 80%
- **接口测试覆盖率**: 100%
- **核心功能测试覆盖率**: 100%
- **性能指标**: 响应时间 < 2秒
- **可用性**: 99.9%
## 🏗️ 测试架构
### 测试金字塔
```mermaid
graph TD
A[UI测试 - 10%] --> B[集成测试 - 20%]
B --> C[单元测试 - 70%]
style A fill:#ff9999
style B fill:#99ccff
style C fill:#99ff99
```
### 测试分层
#### 1. 单元测试 (Unit Testing)
- **目标**: 测试最小可测试单元
- **工具**: Jest, Vitest, JUnit
- **覆盖**: 函数、方法、组件
#### 2. 集成测试 (Integration Testing)
- **目标**: 测试模块间交互
- **工具**: Supertest, TestContainers
- **覆盖**: API接口、数据库交互
#### 3. 端到端测试 (E2E Testing)
- **目标**: 测试完整用户流程
- **工具**: Playwright, Cypress
- **覆盖**: 关键业务流程
#### 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 = ''
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
总行数: ${total.lines.total}
已覆盖行数: ${total.lines.covered}
未覆盖行数: ${total.lines.skipped}