进销存系统自动化测试实践
测试策略概述
进销存系统涉及采购、销售、库存、财务等多个复杂业务模块,自动化测试是保证系统稳定性的关键。本文介绍从单元测试到端到端测试的完整测试实践方案。
测试金字塔
构建合理的测试金字塔,确保测试覆盖和执行效率的平衡:
| 测试类型 | 占比 | 执行时间 | 工具选型 |
|---|---|---|---|
| 单元测试 | 70% | 秒级 | Jest、Mocha |
| 集成测试 | 20% | 分钟级 | Supertest |
| E2E测试 | 10% | 十分钟级 | Playwright、Cypress |
单元测试实践
单元测试关注业务逻辑的最小单元,确保每个函数行为正确:
价格计算单元测试
// priceCalculator.js
function calculatePrice(quantity, unitPrice, discountRate = 0) {
if (quantity <= 0) {
throw new Error('数量必须大于0');
}
if (unitPrice < 0) {
throw new Error('单价不能为负数');
}
if (discountRate < 0 || discountRate > 1) {
throw new Error('折扣率必须在0-1之间');
}
const subtotal = quantity * unitPrice;
const discount = subtotal * discountRate;
return subtotal - discount;
}
// priceCalculator.test.js
describe('calculatePrice', () => {
test('基础价格计算', () => {
expect(calculatePrice(10, 100)).toBe(1000);
});
test('有折扣的价格计算', () => {
expect(calculatePrice(10, 100, 0.1)).toBe(900);
});
test('数量为0抛出异常', () => {
expect(() => calculatePrice(0, 100)).toThrow('数量必须大于0');
});
test('负数单价抛出异常', () => {
expect(() => calculatePrice(10, -50)).toThrow('单价不能为负数');
});
test('折扣率超范围抛出异常', () => {
expect(() => calculatePrice(10, 100, 1.5)).toThrow('折扣率必须在0-1之间');
});
test('浮点数精度处理', () => {
expect(calculatePrice(3, 33.33, 0.1)).toBeCloseTo(89.991, 2);
});
});
库存扣减单元测试
// inventory.js
class InventoryService {
constructor() {
this.inventory = new Map();
}
// 扣减库存
decreaseStock(productId, quantity, warehouseId) {
const key = `${productId}-${warehouseId}`;
const currentStock = this.inventory.get(key) || 0;
if (currentStock < quantity) {
throw new Error(`库存不足,当前库存:${currentStock},需要:${quantity}`);
}
this.inventory.set(key, currentStock - quantity);
return currentStock - quantity;
}
// 批量扣减
batchDecreaseStock(orders, warehouseId) {
const results = [];
const errors = [];
for (const order of orders) {
try {
const result = this.decreaseStock(order.productId, order.quantity, warehouseId);
results.push({ productId: order.productId, success: true, remaining: result });
} catch (err) {
errors.push({ productId: order.productId, error: err.message });
// 库存不足时回滚
this.rollbackDecrease(orders.slice(0, orders.indexOf(order)));
throw new Error(`库存不足,商品ID:${order.productId},已回滚`);
}
}
return { results, errors };
}
}
// inventory.test.js
describe('InventoryService', () => {
let inventory;
beforeEach(() => {
inventory = new InventoryService();
// 初始化库存
inventory.inventory.set('P001-W01', 100);
inventory.inventory.set('P002-W01', 50);
});
test('正常扣减库存', () => {
const result = inventory.decreaseStock('P001', 30, 'W01');
expect(result).toBe(70);
});
test('库存不足抛出异常', () => {
expect(() => inventory.decreaseStock('P001', 150, 'W01')).toThrow('库存不足');
});
test('批量扣减全部成功', () => {
const orders = [
{ productId: 'P001', quantity: 10 },
{ productId: 'P002', quantity: 10 }
];
const { results, errors } = inventory.batchDecreaseStock(orders, 'W01');
expect(errors).toHaveLength(0);
expect(results).toHaveLength(2);
expect(inventory.inventory.get('P001-W01')).toBe(90);
});
test('批量扣减部分失败回滚', () => {
const orders = [
{ productId: 'P001', quantity: 10 },
{ productId: 'P002', quantity: 100 } // 库存不足
];
expect(() => inventory.batchDecreaseStock(orders, 'W01')).toThrow('库存不足');
expect(inventory.inventory.get('P001-W01')).toBe(100); // 已回滚
});
});
集成测试实践
集成测试验证多个组件协作的正确性,使用Supertest测试API接口:
采购订单集成测试
// orders.test.js
const request = require('supertest');
const app = require('../app');
const { setupDatabase, cleanupDatabase } = require('./testHelper');
describe('采购订单API集成测试', () => {
beforeAll(setupDatabase);
afterAll(cleanupDatabase);
describe('POST /api/purchase-orders', () => {
test('创建采购订单成功', async () => {
const response = await request(app)
.post('/api/purchase-orders')
.send({
supplierId: 'S001',
items: [
{ productId: 'P001', quantity: 100, unitPrice: 50 }
],
expectedDate: '2025-07-01'
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.orderNo).toBeDefined();
expect(response.body.data.status).toBe('pending');
});
test('创建订单时库存自动增加', async () => {
const initialStock = await getStock('P001', 'W01');
await request(app)
.post('/api/purchase-orders')
.send({
supplierId: 'S001',
items: [{ productId: 'P001', quantity: 50, unitPrice: 50 }],
status: 'received' // 直接入库
});
const finalStock = await getStock('P001', 'W01');
expect(finalStock - initialStock).toBe(50);
});
test('商品不存在返回400错误', async () => {
const response = await request(app)
.post('/api/purchase-orders')
.send({
supplierId: 'S001',
items: [{ productId: 'NOT_EXIST', quantity: 10, unitPrice: 50 }]
})
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error.code).toBe('PRODUCT_NOT_FOUND');
});
});
describe('GET /api/purchase-orders/:id', () => {
test('查询订单详情', async () => {
// 先创建订单
const created = await createTestOrder();
const orderId = created.body.data.id;
const response = await request(app)
.get(`/api/purchase-orders/${orderId}`)
.expect(200);
expect(response.body.data.id).toBe(orderId);
expect(response.body.data.items).toBeDefined();
});
});
});
销售出库集成测试
// sales.test.js
describe('销售出库流程集成测试', () => {
test('完整销售出库流程', async () => {
// 1. 创建销售订单
const orderResponse = await request(app)
.post('/api/sales-orders')
.send({
customerId: 'C001',
warehouseId: 'W01',
items: [
{ productId: 'P001', quantity: 5 }
]
});
const orderId = orderResponse.body.data.id;
// 2. 审核订单
await request(app)
.post(`/api/sales-orders/${orderId}/approve`)
.send({ approver: 'admin' });
// 3. 出库
const deliveryResponse = await request(app)
.post('/api/deliveries')
.send({
orderId,
deliveryNo: 'D20250612001',
items: [{ productId: 'P001', quantity: 5 }]
});
expect(deliveryResponse.body.success).toBe(true);
// 4. 验证库存扣减
const stockResponse = await request(app)
.get('/api/inventory/P001')
.query({ warehouseId: 'W01' });
expect(stockResponse.body.data.quantity).toBeLessThan(initialStock);
});
test('库存不足阻止出库', async () => {
// 设置低库存
await setStock('P001', 'W01', 3);
// 创建订单需要5个
const orderResponse = await request(app)
.post('/api/sales-orders')
.send({
customerId: 'C001',
warehouseId: 'W01',
items: [{ productId: 'P001', quantity: 5 }]
});
// 出库时应失败
await request(app)
.post('/api/deliveries')
.send({
orderId: orderResponse.body.data.id,
deliveryNo: 'D20250612002',
items: [{ productId: 'P001', quantity: 5 }]
})
.expect(400);
});
});
端到端测试
使用Playwright进行端到端测试,模拟真实用户操作:
采购入库E2E测试
// purchase-flow.e2e.js
const { test, expect } = require('@playwright/test');
test.describe('采购入库完整流程', () => {
test('从创建采购单到入库完成', async ({ page }) => {
// 1. 登录
await page.goto('https://jxc.example.com/login');
await page.fill('#username', 'admin');
await page.fill('#password', 'admin123');
await page.click('#loginBtn');
await expect(page).toHaveURL('/dashboard');
// 2. 进入采购管理
await page.click('text=采购管理');
await page.click('text=创建采购单');
// 3. 填写采购单信息
await page.selectOption('#supplier', '供应商A');
await page.click('.add-item-btn');
// 4. 添加商品
await page.fill('.item-product input', '商品P001');
await page.fill('.item-quantity input', '100');
await page.fill('.item-price input', '50');
await page.click('.confirm-item-btn');
// 5. 提交采购单
await page.click('#submitBtn');
await expect(page.locator('.success-message')).toContainText('提交成功');
// 6. 审核采购单
await page.click('text=待审核');
await page.click('.order-item:first-child .approve-btn');
await page.click('#confirmApprove');
await expect(page.locator('.status')).toContainText('已审核');
// 7. 入库
await page.click('text=入库');
await page.fill('#receiptNo', 'RK20250612001');
await page.click('#confirmReceipt');
await expect(page.locator('.success-message')).toContainText('入库成功');
// 8. 验证库存
await page.click('text=库存查询');
await page.fill('#productSearch', 'P001');
await page.click('#searchBtn');
const stock = await page.locator('.stock-value').textContent();
expect(parseInt(stock)).toBeGreaterThan(0);
});
});
测试数据管理
测试数据的准备和清理是自动化测试的关键:
// testHelper.js
const { DataFactory } = require('./factories');
class TestDataManager {
constructor() {
this.createdIds = [];
}
// 创建测试用户
async createUser(overrides = {}) {
const user = await DataFactory.user({
username: `test_${Date.now()}`,
role: 'admin',
...overrides
});
this.createdIds.push({ type: 'user', id: user.id });
return user;
}
// 创建测试商品
async createProduct(overrides = {}) {
const product = await DataFactory.product({
code: `P${Date.now()}`,
name: `测试商品${Date.now()}`,
...overrides
});
this.createdIds.push({ type: 'product', id: product.id });
return product;
}
// 创建测试仓库
async createWarehouse(overrides = {}) {
const warehouse = await DataFactory.warehouse({
code: `W${Date.now()}`,
name: `测试仓库${Date.now()}`,
...overrides
});
this.createdIds.push({ type: 'warehouse', id: warehouse.id });
return warehouse;
}
// 批量清理测试数据
async cleanup() {
for (const item of this.createdIds.reverse()) {
switch (item.type) {
case 'user': await User.destroy(item.id); break;
case 'product': await Product.destroy(item.id); break;
case 'warehouse': await Warehouse.destroy(item.id); break;
}
}
this.createdIds = [];
}
}
module.exports = new TestDataManager();
持续集成配置
配置CI流水线确保每次提交都运行测试:
# .github/workflows/test.yml
name: Test
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: test
MYSQL_DATABASE: jxc_test
ports:
- 3306:3306
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
env:
DB_HOST: localhost
REDIS_HOST: localhost
- name: Run integration tests
run: npm run test:integration
env:
DB_HOST: localhost
REDIS_HOST: localhost
- name: Upload coverage
uses: codecov/codecov-action@v3
e2e:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Run E2E tests
run: npm run test:e2e
env:
BASE_URL: https://staging.jxc.example.com
总结
进销存系统的自动化测试需要覆盖业务核心逻辑,包括库存管理、价格计算、订单流程等。通过构建完整的测试金字塔,从单元测试到E2E测试,可以有效保障系统稳定性和响应需求变化的能力。