个人编程网站

进销存(JXC)软件开发技术积累与分享

进销存系统自动化测试实践

测试策略概述

进销存系统涉及采购、销售、库存、财务等多个复杂业务模块,自动化测试是保证系统稳定性的关键。本文介绍从单元测试到端到端测试的完整测试实践方案。

测试金字塔

构建合理的测试金字塔,确保测试覆盖和执行效率的平衡:

测试类型 占比 执行时间 工具选型
单元测试 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测试,可以有效保障系统稳定性和响应需求变化的能力。

← 下一篇:进销存系统智能推荐算法设计与实现 上篇:进销存系统数据挖掘与客户价值分析 →