1181 lines
29 KiB
Markdown
1181 lines
29 KiB
Markdown
|
|
# 活牛采购智能数字化系统 - 测试指南
|
|||
|
|
|
|||
|
|
## 版本历史
|
|||
|
|
| 版本 | 日期 | 作者 | 变更说明 |
|
|||
|
|
|------|------|------|----------|
|
|||
|
|
| v1.0 | 2024-12-20 | 测试团队 | 初版测试指南 |
|
|||
|
|
|
|||
|
|
## 1. 测试概述
|
|||
|
|
|
|||
|
|
### 1.1 测试策略
|
|||
|
|
|
|||
|
|
本系统采用多层次测试策略,确保系统质量和稳定性:
|
|||
|
|
|
|||
|
|
- **单元测试**: 测试单个函数和模块
|
|||
|
|
- **集成测试**: 测试模块间的交互
|
|||
|
|
- **API测试**: 测试接口功能和性能
|
|||
|
|
- **端到端测试**: 测试完整业务流程
|
|||
|
|
- **性能测试**: 测试系统性能和负载能力
|
|||
|
|
- **安全测试**: 测试系统安全性
|
|||
|
|
|
|||
|
|
### 1.2 测试环境
|
|||
|
|
|
|||
|
|
#### 测试环境配置
|
|||
|
|
```bash
|
|||
|
|
# 测试环境要求
|
|||
|
|
Node.js >= 18.0.0
|
|||
|
|
MySQL >= 8.0 (测试数据库)
|
|||
|
|
Redis >= 6.0 (测试缓存)
|
|||
|
|
Docker >= 20.10 (容器测试)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 测试数据库配置
|
|||
|
|
```sql
|
|||
|
|
-- 创建测试数据库
|
|||
|
|
CREATE DATABASE niumall_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|||
|
|
|
|||
|
|
-- 创建测试用户
|
|||
|
|
CREATE USER 'test_user'@'localhost' IDENTIFIED BY 'test_password';
|
|||
|
|
GRANT ALL PRIVILEGES ON niumall_test.* TO 'test_user'@'localhost';
|
|||
|
|
FLUSH PRIVILEGES;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 2. 单元测试
|
|||
|
|
|
|||
|
|
### 2.1 测试框架配置
|
|||
|
|
|
|||
|
|
#### 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
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 测试环境配置
|
|||
|
|
```javascript
|
|||
|
|
// tests/setup.js
|
|||
|
|
const { sequelize } = require('../src/models');
|
|||
|
|
|
|||
|
|
// 测试前设置
|
|||
|
|
beforeAll(async () => {
|
|||
|
|
// 连接测试数据库
|
|||
|
|
await sequelize.authenticate();
|
|||
|
|
|
|||
|
|
// 同步数据库结构
|
|||
|
|
await sequelize.sync({ force: true });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 测试后清理
|
|||
|
|
afterAll(async () => {
|
|||
|
|
// 关闭数据库连接
|
|||
|
|
await sequelize.close();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 每个测试前清理数据
|
|||
|
|
beforeEach(async () => {
|
|||
|
|
// 清理测试数据
|
|||
|
|
await sequelize.truncate({ cascade: true });
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 模型测试
|
|||
|
|
|
|||
|
|
#### 用户模型测试
|
|||
|
|
```javascript
|
|||
|
|
// tests/models/user.test.js
|
|||
|
|
const { User } = require('../../src/models');
|
|||
|
|
|
|||
|
|
describe('User Model', () => {
|
|||
|
|
describe('创建用户', () => {
|
|||
|
|
test('应该成功创建有效用户', async () => {
|
|||
|
|
const userData = {
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '测试用户',
|
|||
|
|
role: 'buyer'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const user = await User.create(userData);
|
|||
|
|
|
|||
|
|
expect(user.id).toBeDefined();
|
|||
|
|
expect(user.phone).toBe(userData.phone);
|
|||
|
|
expect(user.name).toBe(userData.name);
|
|||
|
|
expect(user.role).toBe(userData.role);
|
|||
|
|
expect(user.password).not.toBe(userData.password); // 应该被加密
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该拒绝无效手机号', async () => {
|
|||
|
|
const userData = {
|
|||
|
|
phone: '123',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '测试用户',
|
|||
|
|
role: 'buyer'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
await expect(User.create(userData)).rejects.toThrow();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该拒绝重复手机号', async () => {
|
|||
|
|
const userData = {
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '测试用户',
|
|||
|
|
role: 'buyer'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
await User.create(userData);
|
|||
|
|
await expect(User.create(userData)).rejects.toThrow();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('用户验证', () => {
|
|||
|
|
test('应该正确验证密码', async () => {
|
|||
|
|
const password = 'password123';
|
|||
|
|
const user = await User.create({
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password,
|
|||
|
|
name: '测试用户',
|
|||
|
|
role: 'buyer'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const isValid = await user.validatePassword(password);
|
|||
|
|
expect(isValid).toBe(true);
|
|||
|
|
|
|||
|
|
const isInvalid = await user.validatePassword('wrongpassword');
|
|||
|
|
expect(isInvalid).toBe(false);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 订单模型测试
|
|||
|
|
```javascript
|
|||
|
|
// tests/models/order.test.js
|
|||
|
|
const { Order, User } = require('../../src/models');
|
|||
|
|
|
|||
|
|
describe('Order Model', () => {
|
|||
|
|
let buyer, seller;
|
|||
|
|
|
|||
|
|
beforeEach(async () => {
|
|||
|
|
buyer = await User.create({
|
|||
|
|
phone: '13800138001',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '采购商',
|
|||
|
|
role: 'buyer'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
seller = await User.create({
|
|||
|
|
phone: '13800138002',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '养殖户',
|
|||
|
|
role: 'seller'
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该成功创建订单', async () => {
|
|||
|
|
const orderData = {
|
|||
|
|
buyer_id: buyer.id,
|
|||
|
|
seller_id: seller.id,
|
|||
|
|
cattle_type: '肉牛',
|
|||
|
|
quantity: 50,
|
|||
|
|
unit_price: 15000,
|
|||
|
|
total_amount: 750000,
|
|||
|
|
delivery_date: new Date('2024-12-25'),
|
|||
|
|
delivery_address: '测试地址'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const order = await Order.create(orderData);
|
|||
|
|
|
|||
|
|
expect(order.id).toBeDefined();
|
|||
|
|
expect(order.status).toBe('pending');
|
|||
|
|
expect(order.total_amount).toBe(orderData.total_amount);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该正确计算订单总金额', async () => {
|
|||
|
|
const order = await Order.create({
|
|||
|
|
buyer_id: buyer.id,
|
|||
|
|
seller_id: seller.id,
|
|||
|
|
cattle_type: '肉牛',
|
|||
|
|
quantity: 50,
|
|||
|
|
unit_price: 15000,
|
|||
|
|
delivery_date: new Date('2024-12-25'),
|
|||
|
|
delivery_address: '测试地址'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
expect(order.total_amount).toBe(50 * 15000);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.3 服务层测试
|
|||
|
|
|
|||
|
|
#### 用户服务测试
|
|||
|
|
```javascript
|
|||
|
|
// tests/services/userService.test.js
|
|||
|
|
const UserService = require('../../src/services/userService');
|
|||
|
|
const { User } = require('../../src/models');
|
|||
|
|
|
|||
|
|
describe('UserService', () => {
|
|||
|
|
describe('用户注册', () => {
|
|||
|
|
test('应该成功注册新用户', async () => {
|
|||
|
|
const userData = {
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '测试用户',
|
|||
|
|
role: 'buyer'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const result = await UserService.register(userData);
|
|||
|
|
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
expect(result.user.phone).toBe(userData.phone);
|
|||
|
|
expect(result.token).toBeDefined();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该拒绝已存在的手机号', async () => {
|
|||
|
|
const userData = {
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '测试用户',
|
|||
|
|
role: 'buyer'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
await UserService.register(userData);
|
|||
|
|
const result = await UserService.register(userData);
|
|||
|
|
|
|||
|
|
expect(result.success).toBe(false);
|
|||
|
|
expect(result.message).toContain('手机号已存在');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('用户登录', () => {
|
|||
|
|
beforeEach(async () => {
|
|||
|
|
await UserService.register({
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '测试用户',
|
|||
|
|
role: 'buyer'
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该成功登录有效用户', async () => {
|
|||
|
|
const result = await UserService.login('13800138000', 'password123');
|
|||
|
|
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
expect(result.user.phone).toBe('13800138000');
|
|||
|
|
expect(result.token).toBeDefined();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该拒绝错误密码', async () => {
|
|||
|
|
const result = await UserService.login('13800138000', 'wrongpassword');
|
|||
|
|
|
|||
|
|
expect(result.success).toBe(false);
|
|||
|
|
expect(result.message).toContain('密码错误');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该拒绝不存在的用户', async () => {
|
|||
|
|
const result = await UserService.login('13800138999', 'password123');
|
|||
|
|
|
|||
|
|
expect(result.success).toBe(false);
|
|||
|
|
expect(result.message).toContain('用户不存在');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.4 运行单元测试
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 安装测试依赖
|
|||
|
|
npm install --save-dev jest supertest
|
|||
|
|
|
|||
|
|
# 运行所有测试
|
|||
|
|
npm test
|
|||
|
|
|
|||
|
|
# 运行特定测试文件
|
|||
|
|
npm test -- tests/models/user.test.js
|
|||
|
|
|
|||
|
|
# 运行测试并生成覆盖率报告
|
|||
|
|
npm run test:coverage
|
|||
|
|
|
|||
|
|
# 监听模式运行测试
|
|||
|
|
npm run test:watch
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 3. API测试
|
|||
|
|
|
|||
|
|
### 3.1 API测试框架
|
|||
|
|
|
|||
|
|
#### Supertest配置
|
|||
|
|
```javascript
|
|||
|
|
// tests/api/setup.js
|
|||
|
|
const request = require('supertest');
|
|||
|
|
const app = require('../../src/app');
|
|||
|
|
|
|||
|
|
// 创建测试客户端
|
|||
|
|
const api = request(app);
|
|||
|
|
|
|||
|
|
// 辅助函数:获取认证token
|
|||
|
|
async function getAuthToken(userData = {}) {
|
|||
|
|
const defaultUser = {
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '测试用户',
|
|||
|
|
role: 'buyer'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const user = { ...defaultUser, ...userData };
|
|||
|
|
|
|||
|
|
// 注册用户
|
|||
|
|
await api.post('/api/auth/register').send(user);
|
|||
|
|
|
|||
|
|
// 登录获取token
|
|||
|
|
const response = await api
|
|||
|
|
.post('/api/auth/login')
|
|||
|
|
.send({
|
|||
|
|
phone: user.phone,
|
|||
|
|
password: user.password
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return response.body.token;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = { api, getAuthToken };
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 认证API测试
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// tests/api/auth.test.js
|
|||
|
|
const { api } = require('./setup');
|
|||
|
|
|
|||
|
|
describe('Auth API', () => {
|
|||
|
|
describe('POST /api/auth/register', () => {
|
|||
|
|
test('应该成功注册新用户', async () => {
|
|||
|
|
const userData = {
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '测试用户',
|
|||
|
|
role: 'buyer'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const response = await api
|
|||
|
|
.post('/api/auth/register')
|
|||
|
|
.send(userData)
|
|||
|
|
.expect(201);
|
|||
|
|
|
|||
|
|
expect(response.body.success).toBe(true);
|
|||
|
|
expect(response.body.user.phone).toBe(userData.phone);
|
|||
|
|
expect(response.body.token).toBeDefined();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该拒绝无效数据', async () => {
|
|||
|
|
const invalidData = {
|
|||
|
|
phone: '123',
|
|||
|
|
password: '123'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const response = await api
|
|||
|
|
.post('/api/auth/register')
|
|||
|
|
.send(invalidData)
|
|||
|
|
.expect(400);
|
|||
|
|
|
|||
|
|
expect(response.body.success).toBe(false);
|
|||
|
|
expect(response.body.errors).toBeDefined();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('POST /api/auth/login', () => {
|
|||
|
|
beforeEach(async () => {
|
|||
|
|
await api.post('/api/auth/register').send({
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password: 'password123',
|
|||
|
|
name: '测试用户',
|
|||
|
|
role: 'buyer'
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该成功登录', async () => {
|
|||
|
|
const response = await api
|
|||
|
|
.post('/api/auth/login')
|
|||
|
|
.send({
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password: 'password123'
|
|||
|
|
})
|
|||
|
|
.expect(200);
|
|||
|
|
|
|||
|
|
expect(response.body.success).toBe(true);
|
|||
|
|
expect(response.body.token).toBeDefined();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该拒绝错误密码', async () => {
|
|||
|
|
const response = await api
|
|||
|
|
.post('/api/auth/login')
|
|||
|
|
.send({
|
|||
|
|
phone: '13800138000',
|
|||
|
|
password: 'wrongpassword'
|
|||
|
|
})
|
|||
|
|
.expect(401);
|
|||
|
|
|
|||
|
|
expect(response.body.success).toBe(false);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 订单API测试
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// tests/api/orders.test.js
|
|||
|
|
const { api, getAuthToken } = require('./setup');
|
|||
|
|
|
|||
|
|
describe('Orders API', () => {
|
|||
|
|
let buyerToken, sellerToken;
|
|||
|
|
|
|||
|
|
beforeEach(async () => {
|
|||
|
|
buyerToken = await getAuthToken({
|
|||
|
|
phone: '13800138001',
|
|||
|
|
role: 'buyer'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
sellerToken = await getAuthToken({
|
|||
|
|
phone: '13800138002',
|
|||
|
|
role: 'seller'
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('POST /api/orders', () => {
|
|||
|
|
test('采购商应该能创建订单', async () => {
|
|||
|
|
const orderData = {
|
|||
|
|
seller_phone: '13800138002',
|
|||
|
|
cattle_type: '肉牛',
|
|||
|
|
quantity: 50,
|
|||
|
|
unit_price: 15000,
|
|||
|
|
delivery_date: '2024-12-25',
|
|||
|
|
delivery_address: '测试地址'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const response = await api
|
|||
|
|
.post('/api/orders')
|
|||
|
|
.set('Authorization', `Bearer ${buyerToken}`)
|
|||
|
|
.send(orderData)
|
|||
|
|
.expect(201);
|
|||
|
|
|
|||
|
|
expect(response.body.success).toBe(true);
|
|||
|
|
expect(response.body.order.status).toBe('pending');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该拒绝未认证的请求', async () => {
|
|||
|
|
const orderData = {
|
|||
|
|
seller_phone: '13800138002',
|
|||
|
|
cattle_type: '肉牛',
|
|||
|
|
quantity: 50,
|
|||
|
|
unit_price: 15000
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
await api
|
|||
|
|
.post('/api/orders')
|
|||
|
|
.send(orderData)
|
|||
|
|
.expect(401);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('GET /api/orders', () => {
|
|||
|
|
test('应该返回用户的订单列表', async () => {
|
|||
|
|
// 先创建一个订单
|
|||
|
|
await api
|
|||
|
|
.post('/api/orders')
|
|||
|
|
.set('Authorization', `Bearer ${buyerToken}`)
|
|||
|
|
.send({
|
|||
|
|
seller_phone: '13800138002',
|
|||
|
|
cattle_type: '肉牛',
|
|||
|
|
quantity: 50,
|
|||
|
|
unit_price: 15000,
|
|||
|
|
delivery_date: '2024-12-25',
|
|||
|
|
delivery_address: '测试地址'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const response = await api
|
|||
|
|
.get('/api/orders')
|
|||
|
|
.set('Authorization', `Bearer ${buyerToken}`)
|
|||
|
|
.expect(200);
|
|||
|
|
|
|||
|
|
expect(response.body.success).toBe(true);
|
|||
|
|
expect(Array.isArray(response.body.orders)).toBe(true);
|
|||
|
|
expect(response.body.orders.length).toBeGreaterThan(0);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 4. 端到端测试
|
|||
|
|
|
|||
|
|
### 4.1 Cypress配置
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// cypress.config.js
|
|||
|
|
const { defineConfig } = require('cypress');
|
|||
|
|
|
|||
|
|
module.exports = defineConfig({
|
|||
|
|
e2e: {
|
|||
|
|
baseUrl: 'http://localhost:3000',
|
|||
|
|
supportFile: 'cypress/support/e2e.js',
|
|||
|
|
specPattern: 'cypress/e2e/**/*.cy.js',
|
|||
|
|
video: true,
|
|||
|
|
screenshot: true,
|
|||
|
|
viewportWidth: 1280,
|
|||
|
|
viewportHeight: 720,
|
|||
|
|
defaultCommandTimeout: 10000,
|
|||
|
|
requestTimeout: 10000,
|
|||
|
|
responseTimeout: 10000
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 用户注册登录流程测试
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// cypress/e2e/auth.cy.js
|
|||
|
|
describe('用户认证流程', () => {
|
|||
|
|
beforeEach(() => {
|
|||
|
|
// 清理数据库
|
|||
|
|
cy.task('db:seed');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('应该完成完整的注册登录流程', () => {
|
|||
|
|
// 访问注册页面
|
|||
|
|
cy.visit('/register');
|
|||
|
|
|
|||
|
|
// 填写注册表单
|
|||
|
|
cy.get('[data-cy=phone-input]').type('13800138000');
|
|||
|
|
cy.get('[data-cy=password-input]').type('password123');
|
|||
|
|
cy.get('[data-cy=name-input]').type('测试用户');
|
|||
|
|
cy.get('[data-cy=role-select]').select('buyer');
|
|||
|
|
|
|||
|
|
// 提交注册
|
|||
|
|
cy.get('[data-cy=register-button]').click();
|
|||
|
|
|
|||
|
|
// 验证注册成功
|
|||
|
|
cy.url().should('include', '/dashboard');
|
|||
|
|
cy.get('[data-cy=user-name]').should('contain', '测试用户');
|
|||
|
|
|
|||
|
|
// 退出登录
|
|||
|
|
cy.get('[data-cy=logout-button]').click();
|
|||
|
|
|
|||
|
|
// 重新登录
|
|||
|
|
cy.visit('/login');
|
|||
|
|
cy.get('[data-cy=phone-input]').type('13800138000');
|
|||
|
|
cy.get('[data-cy=password-input]').type('password123');
|
|||
|
|
cy.get('[data-cy=login-button]').click();
|
|||
|
|
|
|||
|
|
// 验证登录成功
|
|||
|
|
cy.url().should('include', '/dashboard');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.3 订单创建流程测试
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// cypress/e2e/orders.cy.js
|
|||
|
|
describe('订单管理流程', () => {
|
|||
|
|
beforeEach(() => {
|
|||
|
|
cy.task('db:seed');
|
|||
|
|
// 登录为采购商
|
|||
|
|
cy.login('buyer');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('应该完成完整的订单创建流程', () => {
|
|||
|
|
// 访问订单创建页面
|
|||
|
|
cy.visit('/orders/create');
|
|||
|
|
|
|||
|
|
// 填写订单信息
|
|||
|
|
cy.get('[data-cy=seller-select]').select('养殖户A');
|
|||
|
|
cy.get('[data-cy=cattle-type-input]').type('肉牛');
|
|||
|
|
cy.get('[data-cy=quantity-input]').type('50');
|
|||
|
|
cy.get('[data-cy=unit-price-input]').type('15000');
|
|||
|
|
cy.get('[data-cy=delivery-date-input]').type('2024-12-25');
|
|||
|
|
cy.get('[data-cy=delivery-address-input]').type('测试地址');
|
|||
|
|
|
|||
|
|
// 提交订单
|
|||
|
|
cy.get('[data-cy=create-order-button]').click();
|
|||
|
|
|
|||
|
|
// 验证订单创建成功
|
|||
|
|
cy.get('[data-cy=success-message]').should('be.visible');
|
|||
|
|
cy.url().should('include', '/orders');
|
|||
|
|
|
|||
|
|
// 验证订单出现在列表中
|
|||
|
|
cy.get('[data-cy=order-list]').should('contain', '肉牛');
|
|||
|
|
cy.get('[data-cy=order-status]').should('contain', '待确认');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('应该能查看订单详情', () => {
|
|||
|
|
// 创建测试订单
|
|||
|
|
cy.createTestOrder();
|
|||
|
|
|
|||
|
|
// 访问订单列表
|
|||
|
|
cy.visit('/orders');
|
|||
|
|
|
|||
|
|
// 点击查看详情
|
|||
|
|
cy.get('[data-cy=order-item]').first().click();
|
|||
|
|
|
|||
|
|
// 验证详情页面
|
|||
|
|
cy.url().should('include', '/orders/');
|
|||
|
|
cy.get('[data-cy=order-details]').should('be.visible');
|
|||
|
|
cy.get('[data-cy=cattle-type]').should('contain', '肉牛');
|
|||
|
|
cy.get('[data-cy=quantity]').should('contain', '50');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. 性能测试
|
|||
|
|
|
|||
|
|
### 5.1 负载测试配置
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// tests/performance/load-test.js
|
|||
|
|
import http from 'k6/http';
|
|||
|
|
import { check, sleep } from 'k6';
|
|||
|
|
import { Rate } from 'k6/metrics';
|
|||
|
|
|
|||
|
|
// 自定义指标
|
|||
|
|
export let errorRate = new Rate('errors');
|
|||
|
|
|
|||
|
|
// 测试配置
|
|||
|
|
export let options = {
|
|||
|
|
stages: [
|
|||
|
|
{ duration: '2m', target: 100 }, // 2分钟内增加到100用户
|
|||
|
|
{ duration: '5m', target: 100 }, // 保持100用户5分钟
|
|||
|
|
{ duration: '2m', target: 200 }, // 2分钟内增加到200用户
|
|||
|
|
{ duration: '5m', target: 200 }, // 保持200用户5分钟
|
|||
|
|
{ duration: '2m', target: 0 }, // 2分钟内减少到0用户
|
|||
|
|
],
|
|||
|
|
thresholds: {
|
|||
|
|
http_req_duration: ['p(99)<1500'], // 99%的请求在1.5秒内完成
|
|||
|
|
http_req_failed: ['rate<0.1'], // 错误率小于10%
|
|||
|
|
errors: ['rate<0.1'], // 自定义错误率小于10%
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 测试数据
|
|||
|
|
const users = [
|
|||
|
|
{ phone: '13800138001', password: 'password123' },
|
|||
|
|
{ phone: '13800138002', password: 'password123' },
|
|||
|
|
{ phone: '13800138003', password: 'password123' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
export default function () {
|
|||
|
|
// 随机选择用户
|
|||
|
|
const user = users[Math.floor(Math.random() * users.length)];
|
|||
|
|
|
|||
|
|
// 登录
|
|||
|
|
let loginResponse = http.post('http://localhost:3000/api/auth/login', {
|
|||
|
|
phone: user.phone,
|
|||
|
|
password: user.password,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
let loginSuccess = check(loginResponse, {
|
|||
|
|
'登录状态码为200': (r) => r.status === 200,
|
|||
|
|
'登录响应时间<500ms': (r) => r.timings.duration < 500,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
errorRate.add(!loginSuccess);
|
|||
|
|
|
|||
|
|
if (loginSuccess) {
|
|||
|
|
const token = JSON.parse(loginResponse.body).token;
|
|||
|
|
|
|||
|
|
// 获取订单列表
|
|||
|
|
let ordersResponse = http.get('http://localhost:3000/api/orders', {
|
|||
|
|
headers: { Authorization: `Bearer ${token}` },
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
let ordersSuccess = check(ordersResponse, {
|
|||
|
|
'订单列表状态码为200': (r) => r.status === 200,
|
|||
|
|
'订单列表响应时间<1000ms': (r) => r.timings.duration < 1000,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
errorRate.add(!ordersSuccess);
|
|||
|
|
|
|||
|
|
// 创建订单
|
|||
|
|
let createOrderResponse = http.post(
|
|||
|
|
'http://localhost:3000/api/orders',
|
|||
|
|
JSON.stringify({
|
|||
|
|
seller_phone: '13800138002',
|
|||
|
|
cattle_type: '肉牛',
|
|||
|
|
quantity: Math.floor(Math.random() * 100) + 1,
|
|||
|
|
unit_price: 15000,
|
|||
|
|
delivery_date: '2024-12-25',
|
|||
|
|
delivery_address: '测试地址',
|
|||
|
|
}),
|
|||
|
|
{
|
|||
|
|
headers: {
|
|||
|
|
Authorization: `Bearer ${token}`,
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let createSuccess = check(createOrderResponse, {
|
|||
|
|
'创建订单状态码为201': (r) => r.status === 201,
|
|||
|
|
'创建订单响应时间<2000ms': (r) => r.timings.duration < 2000,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
errorRate.add(!createSuccess);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sleep(1);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 数据库性能测试
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 数据库性能测试脚本
|
|||
|
|
-- tests/performance/db-performance.sql
|
|||
|
|
|
|||
|
|
-- 创建测试数据
|
|||
|
|
DELIMITER //
|
|||
|
|
CREATE PROCEDURE GenerateTestData()
|
|||
|
|
BEGIN
|
|||
|
|
DECLARE i INT DEFAULT 1;
|
|||
|
|
|
|||
|
|
-- 创建10000个用户
|
|||
|
|
WHILE i <= 10000 DO
|
|||
|
|
INSERT INTO users (phone, password, name, role, created_at, updated_at)
|
|||
|
|
VALUES (
|
|||
|
|
CONCAT('138', LPAD(i, 8, '0')),
|
|||
|
|
'$2b$10$hash',
|
|||
|
|
CONCAT('用户', i),
|
|||
|
|
CASE WHEN i % 3 = 0 THEN 'seller' ELSE 'buyer' END,
|
|||
|
|
NOW(),
|
|||
|
|
NOW()
|
|||
|
|
);
|
|||
|
|
SET i = i + 1;
|
|||
|
|
END WHILE;
|
|||
|
|
|
|||
|
|
-- 创建50000个订单
|
|||
|
|
SET i = 1;
|
|||
|
|
WHILE i <= 50000 DO
|
|||
|
|
INSERT INTO orders (
|
|||
|
|
buyer_id, seller_id, cattle_type, quantity, unit_price,
|
|||
|
|
total_amount, status, delivery_date, delivery_address,
|
|||
|
|
created_at, updated_at
|
|||
|
|
)
|
|||
|
|
VALUES (
|
|||
|
|
FLOOR(RAND() * 6667) + 1, -- 随机买家
|
|||
|
|
FLOOR(RAND() * 3333) + 6668, -- 随机卖家
|
|||
|
|
CASE FLOOR(RAND() * 3)
|
|||
|
|
WHEN 0 THEN '肉牛'
|
|||
|
|
WHEN 1 THEN '奶牛'
|
|||
|
|
ELSE '种牛'
|
|||
|
|
END,
|
|||
|
|
FLOOR(RAND() * 100) + 1,
|
|||
|
|
FLOOR(RAND() * 5000) + 10000,
|
|||
|
|
0, -- 将通过触发器计算
|
|||
|
|
CASE FLOOR(RAND() * 5)
|
|||
|
|
WHEN 0 THEN 'pending'
|
|||
|
|
WHEN 1 THEN 'confirmed'
|
|||
|
|
WHEN 2 THEN 'in_transit'
|
|||
|
|
WHEN 3 THEN 'delivered'
|
|||
|
|
ELSE 'completed'
|
|||
|
|
END,
|
|||
|
|
DATE_ADD(NOW(), INTERVAL FLOOR(RAND() * 30) DAY),
|
|||
|
|
CONCAT('地址', i),
|
|||
|
|
NOW() - INTERVAL FLOOR(RAND() * 365) DAY,
|
|||
|
|
NOW()
|
|||
|
|
);
|
|||
|
|
SET i = i + 1;
|
|||
|
|
END WHILE;
|
|||
|
|
END //
|
|||
|
|
DELIMITER ;
|
|||
|
|
|
|||
|
|
-- 执行数据生成
|
|||
|
|
CALL GenerateTestData();
|
|||
|
|
|
|||
|
|
-- 性能测试查询
|
|||
|
|
-- 1. 测试订单列表查询性能
|
|||
|
|
EXPLAIN ANALYZE
|
|||
|
|
SELECT o.*, u1.name as buyer_name, u2.name as seller_name
|
|||
|
|
FROM orders o
|
|||
|
|
JOIN users u1 ON o.buyer_id = u1.id
|
|||
|
|
JOIN users u2 ON o.seller_id = u2.id
|
|||
|
|
WHERE o.status = 'pending'
|
|||
|
|
ORDER BY o.created_at DESC
|
|||
|
|
LIMIT 20;
|
|||
|
|
|
|||
|
|
-- 2. 测试用户订单统计性能
|
|||
|
|
EXPLAIN ANALYZE
|
|||
|
|
SELECT
|
|||
|
|
u.id,
|
|||
|
|
u.name,
|
|||
|
|
COUNT(o.id) as order_count,
|
|||
|
|
SUM(o.total_amount) as total_amount
|
|||
|
|
FROM users u
|
|||
|
|
LEFT JOIN orders o ON u.id = o.buyer_id
|
|||
|
|
WHERE u.role = 'buyer'
|
|||
|
|
GROUP BY u.id, u.name
|
|||
|
|
ORDER BY total_amount DESC
|
|||
|
|
LIMIT 100;
|
|||
|
|
|
|||
|
|
-- 3. 测试复杂查询性能
|
|||
|
|
EXPLAIN ANALYZE
|
|||
|
|
SELECT
|
|||
|
|
DATE(o.created_at) as order_date,
|
|||
|
|
o.cattle_type,
|
|||
|
|
COUNT(*) as order_count,
|
|||
|
|
AVG(o.unit_price) as avg_price,
|
|||
|
|
SUM(o.total_amount) as total_amount
|
|||
|
|
FROM orders o
|
|||
|
|
WHERE o.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
|||
|
|
AND o.status IN ('completed', 'delivered')
|
|||
|
|
GROUP BY DATE(o.created_at), o.cattle_type
|
|||
|
|
ORDER BY order_date DESC, total_amount DESC;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 6. 安全测试
|
|||
|
|
|
|||
|
|
### 6.1 SQL注入测试
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// tests/security/sql-injection.test.js
|
|||
|
|
const { api, getAuthToken } = require('../api/setup');
|
|||
|
|
|
|||
|
|
describe('SQL注入安全测试', () => {
|
|||
|
|
let token;
|
|||
|
|
|
|||
|
|
beforeEach(async () => {
|
|||
|
|
token = await getAuthToken();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该防止登录接口SQL注入', async () => {
|
|||
|
|
const maliciousInputs = [
|
|||
|
|
"' OR '1'='1",
|
|||
|
|
"'; DROP TABLE users; --",
|
|||
|
|
"' UNION SELECT * FROM users --",
|
|||
|
|
"admin'--",
|
|||
|
|
"' OR 1=1#"
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const input of maliciousInputs) {
|
|||
|
|
const response = await api
|
|||
|
|
.post('/api/auth/login')
|
|||
|
|
.send({
|
|||
|
|
phone: input,
|
|||
|
|
password: input
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 应该返回401而不是500或其他错误
|
|||
|
|
expect([400, 401]).toContain(response.status);
|
|||
|
|
expect(response.body.success).toBe(false);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该防止订单查询SQL注入', async () => {
|
|||
|
|
const maliciousQueries = [
|
|||
|
|
"1' OR '1'='1",
|
|||
|
|
"1; DROP TABLE orders; --",
|
|||
|
|
"1 UNION SELECT * FROM users"
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const query of maliciousQueries) {
|
|||
|
|
const response = await api
|
|||
|
|
.get(`/api/orders/${query}`)
|
|||
|
|
.set('Authorization', `Bearer ${token}`);
|
|||
|
|
|
|||
|
|
// 应该返回400或404,不应该执行恶意SQL
|
|||
|
|
expect([400, 404]).toContain(response.status);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 XSS测试
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// tests/security/xss.test.js
|
|||
|
|
const { api, getAuthToken } = require('../api/setup');
|
|||
|
|
|
|||
|
|
describe('XSS安全测试', () => {
|
|||
|
|
let token;
|
|||
|
|
|
|||
|
|
beforeEach(async () => {
|
|||
|
|
token = await getAuthToken();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该过滤用户输入中的脚本', async () => {
|
|||
|
|
const xssPayloads = [
|
|||
|
|
'<script>alert("xss")</script>',
|
|||
|
|
'<img src="x" onerror="alert(1)">',
|
|||
|
|
'javascript:alert("xss")',
|
|||
|
|
'<svg onload="alert(1)">',
|
|||
|
|
'"><script>alert("xss")</script>'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const payload of xssPayloads) {
|
|||
|
|
const response = await api
|
|||
|
|
.post('/api/orders')
|
|||
|
|
.set('Authorization', `Bearer ${token}`)
|
|||
|
|
.send({
|
|||
|
|
seller_phone: '13800138002',
|
|||
|
|
cattle_type: payload,
|
|||
|
|
quantity: 50,
|
|||
|
|
unit_price: 15000,
|
|||
|
|
delivery_date: '2024-12-25',
|
|||
|
|
delivery_address: payload
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.status === 201) {
|
|||
|
|
// 如果创建成功,检查返回的数据是否被正确转义
|
|||
|
|
expect(response.body.order.cattle_type).not.toContain('<script>');
|
|||
|
|
expect(response.body.order.delivery_address).not.toContain('<script>');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.3 认证授权测试
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// tests/security/auth.test.js
|
|||
|
|
const { api, getAuthToken } = require('../api/setup');
|
|||
|
|
|
|||
|
|
describe('认证授权安全测试', () => {
|
|||
|
|
test('应该拒绝无效token', async () => {
|
|||
|
|
const invalidTokens = [
|
|||
|
|
'invalid-token',
|
|||
|
|
'Bearer invalid',
|
|||
|
|
'',
|
|||
|
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid',
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const token of invalidTokens) {
|
|||
|
|
const response = await api
|
|||
|
|
.get('/api/orders')
|
|||
|
|
.set('Authorization', token);
|
|||
|
|
|
|||
|
|
expect(response.status).toBe(401);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该防止权限提升', async () => {
|
|||
|
|
// 创建普通用户token
|
|||
|
|
const userToken = await getAuthToken({ role: 'buyer' });
|
|||
|
|
|
|||
|
|
// 尝试访问管理员接口
|
|||
|
|
const response = await api
|
|||
|
|
.get('/api/admin/users')
|
|||
|
|
.set('Authorization', `Bearer ${userToken}`);
|
|||
|
|
|
|||
|
|
expect(response.status).toBe(403);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('应该防止越权访问', async () => {
|
|||
|
|
// 创建两个用户
|
|||
|
|
const user1Token = await getAuthToken({
|
|||
|
|
phone: '13800138001',
|
|||
|
|
role: 'buyer'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const user2Token = await getAuthToken({
|
|||
|
|
phone: '13800138002',
|
|||
|
|
role: 'buyer'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 用户1创建订单
|
|||
|
|
const orderResponse = await api
|
|||
|
|
.post('/api/orders')
|
|||
|
|
.set('Authorization', `Bearer ${user1Token}`)
|
|||
|
|
.send({
|
|||
|
|
seller_phone: '13800138003',
|
|||
|
|
cattle_type: '肉牛',
|
|||
|
|
quantity: 50,
|
|||
|
|
unit_price: 15000,
|
|||
|
|
delivery_date: '2024-12-25',
|
|||
|
|
delivery_address: '测试地址'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const orderId = orderResponse.body.order.id;
|
|||
|
|
|
|||
|
|
// 用户2尝试访问用户1的订单
|
|||
|
|
const accessResponse = await api
|
|||
|
|
.get(`/api/orders/${orderId}`)
|
|||
|
|
.set('Authorization', `Bearer ${user2Token}`);
|
|||
|
|
|
|||
|
|
expect(accessResponse.status).toBe(403);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 7. 测试执行
|
|||
|
|
|
|||
|
|
### 7.1 测试脚本配置
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"scripts": {
|
|||
|
|
"test": "jest",
|
|||
|
|
"test:watch": "jest --watch",
|
|||
|
|
"test:coverage": "jest --coverage",
|
|||
|
|
"test:unit": "jest tests/unit",
|
|||
|
|
"test:integration": "jest tests/integration",
|
|||
|
|
"test:api": "jest tests/api",
|
|||
|
|
"test:security": "jest tests/security",
|
|||
|
|
"test:e2e": "cypress run",
|
|||
|
|
"test:e2e:open": "cypress open",
|
|||
|
|
"test:performance": "k6 run tests/performance/load-test.js",
|
|||
|
|
"test:all": "npm run test:unit && npm run test:api && npm run test:security && npm run test:e2e"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.2 CI/CD集成
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# .github/workflows/test.yml
|
|||
|
|
name: 测试流水线
|
|||
|
|
|
|||
|
|
on:
|
|||
|
|
push:
|
|||
|
|
branches: [ main, develop ]
|
|||
|
|
pull_request:
|
|||
|
|
branches: [ main ]
|
|||
|
|
|
|||
|
|
jobs:
|
|||
|
|
test:
|
|||
|
|
runs-on: ubuntu-latest
|
|||
|
|
|
|||
|
|
services:
|
|||
|
|
mysql:
|
|||
|
|
image: mysql:8.0
|
|||
|
|
env:
|
|||
|
|
MYSQL_ROOT_PASSWORD: root
|
|||
|
|
MYSQL_DATABASE: niumall_test
|
|||
|
|
options: >-
|
|||
|
|
--health-cmd="mysqladmin ping"
|
|||
|
|
--health-interval=10s
|
|||
|
|
--health-timeout=5s
|
|||
|
|
--health-retries=3
|
|||
|
|
|
|||
|
|
redis:
|
|||
|
|
image: redis:7-alpine
|
|||
|
|
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: npm ci
|
|||
|
|
|
|||
|
|
- name: 运行单元测试
|
|||
|
|
run: npm run test:unit
|
|||
|
|
env:
|
|||
|
|
NODE_ENV: test
|
|||
|
|
DB_HOST: localhost
|
|||
|
|
DB_PORT: 3306
|
|||
|
|
DB_NAME: niumall_test
|
|||
|
|
DB_USER: root
|
|||
|
|
DB_PASSWORD: root
|
|||
|
|
REDIS_HOST: localhost
|
|||
|
|
REDIS_PORT: 6379
|
|||
|
|
|
|||
|
|
- name: 运行API测试
|
|||
|
|
run: npm run test:api
|
|||
|
|
env:
|
|||
|
|
NODE_ENV: test
|
|||
|
|
|
|||
|
|
- name: 运行安全测试
|
|||
|
|
run: npm run test:security
|
|||
|
|
env:
|
|||
|
|
NODE_ENV: test
|
|||
|
|
|
|||
|
|
- name: 生成测试报告
|
|||
|
|
run: npm run test:coverage
|
|||
|
|
env:
|
|||
|
|
NODE_ENV: test
|
|||
|
|
|
|||
|
|
- name: 上传覆盖率报告
|
|||
|
|
uses: codecov/codecov-action@v3
|
|||
|
|
with:
|
|||
|
|
file: ./coverage/lcov.info
|
|||
|
|
|
|||
|
|
- name: 运行端到端测试
|
|||
|
|
run: npm run test:e2e
|
|||
|
|
env:
|
|||
|
|
NODE_ENV: test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 8. 测试报告
|
|||
|
|
|
|||
|
|
### 8.1 测试覆盖率要求
|
|||
|
|
|
|||
|
|
- **代码覆盖率**: ≥ 80%
|
|||
|
|
- **分支覆盖率**: ≥ 75%
|
|||
|
|
- **函数覆盖率**: ≥ 85%
|
|||
|
|
- **行覆盖率**: ≥ 80%
|
|||
|
|
|
|||
|
|
### 8.2 性能基准
|
|||
|
|
|
|||
|
|
- **API响应时间**: P99 < 1.5秒
|
|||
|
|
- **数据库查询**: P95 < 500ms
|
|||
|
|
- **并发用户**: 支持200并发用户
|
|||
|
|
- **错误率**: < 1%
|
|||
|
|
|
|||
|
|
### 8.3 测试报告生成
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 生成HTML测试报告
|
|||
|
|
npm run test:coverage
|
|||
|
|
|
|||
|
|
# 生成性能测试报告
|
|||
|
|
k6 run --out json=performance-report.json tests/performance/load-test.js
|
|||
|
|
|
|||
|
|
# 生成安全测试报告
|
|||
|
|
npm run test:security -- --reporters=json --outputFile=security-report.json
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 9. 联系信息
|
|||
|
|
|
|||
|
|
### 9.1 测试团队
|
|||
|
|
|
|||
|
|
- **测试负责人**: test-lead@niumall.com
|
|||
|
|
- **自动化测试**: automation@niumall.com
|
|||
|
|
- **性能测试**: performance@niumall.com
|
|||
|
|
|
|||
|
|
### 9.2 相关文档
|
|||
|
|
|
|||
|
|
- [项目总览](./项目总览.md)
|
|||
|
|
- [产品需求文档](./产品需求文档.md)
|
|||
|
|
- [系统架构设计](./系统架构设计.md)
|
|||
|
|
- [部署运维指南](./部署运维指南.md)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**注意**: 请确保在生产环境部署前通过所有测试用例,并达到覆盖率要求。
|