# 活牛采购智能数字化系统 - 测试指南 ## 版本历史 | 版本 | 日期 | 作者 | 变更说明 | |------|------|------|----------| | 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: ['/src', '/tests'], testMatch: [ '**/__tests__/**/*.js', '**/?(*.)+(spec|test).js' ], collectCoverageFrom: [ 'src/**/*.js', '!src/**/*.test.js', '!src/config/**', '!src/migrations/**' ], coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html'], setupFilesAfterEnv: ['/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 = [ '', '', 'javascript:alert("xss")', '', '">' ]; 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('