Skip to content

写单元测试

问题描述

有一个复杂的订单折扣计算函数,需要覆盖各种边界情况。手动想测试用例容易遗漏,而且写测试代码很繁琐。

java
public BigDecimal calculateDiscount(Order order, User user, Coupon coupon) {
    // 复杂的折扣计算逻辑
    // 涉及:会员等级、订单金额、优惠券类型、商品类目等
}

传统思路

  1. 看代码想用例 - 容易遗漏边界情况
  2. 复制粘贴改参数 - 测试代码冗长重复
  3. 只测正常流程 - 覆盖率不够
  4. 临时加用例 - 出bug后补测试

结果:测试覆盖不全,后期维护困难

AI思维

第一步:让 AI 分析测试场景

请帮我为这个折扣计算函数设计测试用例。

```java
public BigDecimal calculateDiscount(Order order, User user, Coupon coupon) {
    BigDecimal discount = BigDecimal.ZERO;

    // 1. 会员折扣
    if (user.getLevel() == VipLevel.GOLD) {
        discount = discount.add(order.getAmount().multiply(new BigDecimal("0.05")));
    } else if (user.getLevel() == VipLevel.PLATINUM) {
        discount = discount.add(order.getAmount().multiply(new BigDecimal("0.10")));
    }

    // 2. 满减
    if (order.getAmount().compareTo(new BigDecimal("200")) >= 0) {
        discount = discount.add(new BigDecimal("20"));
    }

    // 3. 优惠券
    if (coupon != null && coupon.isValid()) {
        if (coupon.getType() == CouponType.FIXED) {
            discount = discount.add(coupon.getValue());
        } else if (coupon.getType() == CouponType.PERCENT) {
            discount = discount.add(order.getAmount().multiply(coupon.getValue()));
        }
    }

    // 4. 折扣不能超过订单金额
    return discount.min(order.getAmount());
}

请列出所有需要测试的场景,包括边界情况。


AI 分析测试矩阵:

> **测试场景矩阵**
>
> | 类别 | 场景 | 预期结果 |
> |-----|------|---------|
> | **会员折扣** | 普通用户 | 无会员折扣 |
> | | 金卡用户 | 5%折扣 |
> | | 白金用户 | 10%折扣 |
> | **满减** | 订单金额 < 200 | 无满减 |
> | | 订单金额 = 200 | 满减20 |
> | | 订单金额 > 200 | 满减20 |
> | **优惠券** | 无优惠券 (null) | 不报错,无优惠券折扣 |
> | | 优惠券已过期 | 无优惠券折扣 |
> | | 固定金额券 | 减固定金额 |
> | | 百分比券 | 按比例折扣 |
> | **边界情况** | 折扣 > 订单金额 | 折扣 = 订单金额 |
> | | 订单金额 = 0 | 折扣 = 0 |
> | | 多种折扣叠加 | 正确累加 |
> | **组合场景** | 白金+满减+优惠券 | 三种折扣累加 |
>
> **边界值重点测试:**
> - 金额边界:0, 199.99, 200, 200.01
> - 百分比边界:0%, 100%
> - 折扣超限:订单100元,折扣计算出150元

### 第二步:生成测试代码

请用 JUnit 5 + AssertJ 生成完整的测试代码,使用参数化测试减少重复。


AI 生成测试代码:

```java
@ExtendWith(MockitoExtension.class)
class DiscountCalculatorTest {

    private DiscountCalculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new DiscountCalculator();
    }

    // ==================== 会员折扣测试 ====================

    @ParameterizedTest
    @CsvSource({
        "NORMAL, 100, 0",       // 普通用户无折扣
        "GOLD, 100, 5",         // 金卡5%
        "PLATINUM, 100, 10",    // 白金10%
        "GOLD, 200, 10",        // 金卡200元
        "PLATINUM, 200, 20"     // 白金200元
    })
    @DisplayName("会员折扣计算")
    void testMemberDiscount(VipLevel level, int amount, int expectedDiscount) {
        Order order = createOrder(amount);
        User user = createUser(level);

        BigDecimal discount = calculator.calculateDiscount(order, user, null);

        assertThat(discount).isEqualByComparingTo(String.valueOf(expectedDiscount));
    }

    // ==================== 满减测试 ====================

    @ParameterizedTest
    @CsvSource({
        "199.99, 0",    // 不满200
        "200, 20",      // 刚好200
        "200.01, 20",   // 超过200
        "500, 20"       // 远超200
    })
    @DisplayName("满减计算")
    void testFullReduction(String amount, int expectedReduction) {
        Order order = createOrder(new BigDecimal(amount));
        User user = createUser(VipLevel.NORMAL);

        BigDecimal discount = calculator.calculateDiscount(order, user, null);

        assertThat(discount).isEqualByComparingTo(String.valueOf(expectedReduction));
    }

    // ==================== 优惠券测试 ====================

    @Test
    @DisplayName("无优惠券时不报错")
    void testNullCoupon() {
        Order order = createOrder(100);
        User user = createUser(VipLevel.NORMAL);

        BigDecimal discount = calculator.calculateDiscount(order, user, null);

        assertThat(discount).isEqualByComparingTo("0");
    }

    @Test
    @DisplayName("过期优惠券不生效")
    void testExpiredCoupon() {
        Order order = createOrder(100);
        User user = createUser(VipLevel.NORMAL);
        Coupon expiredCoupon = createCoupon(CouponType.FIXED, "10", false);

        BigDecimal discount = calculator.calculateDiscount(order, user, expiredCoupon);

        assertThat(discount).isEqualByComparingTo("0");
    }

    @ParameterizedTest
    @CsvSource({
        "FIXED, 10, 100, 10",     // 固定10元券
        "FIXED, 50, 100, 50",     // 固定50元券
        "PERCENT, 0.1, 100, 10",  // 10%券
        "PERCENT, 0.2, 200, 40"   // 20%券
    })
    @DisplayName("优惠券折扣计算")
    void testCouponDiscount(CouponType type, String couponValue,
                            int orderAmount, int expectedDiscount) {
        Order order = createOrder(orderAmount);
        User user = createUser(VipLevel.NORMAL);
        Coupon coupon = createCoupon(type, couponValue, true);

        BigDecimal discount = calculator.calculateDiscount(order, user, coupon);

        assertThat(discount).isEqualByComparingTo(String.valueOf(expectedDiscount));
    }

    // ==================== 边界情况测试 ====================

    @Test
    @DisplayName("折扣不能超过订单金额")
    void testDiscountCannotExceedOrderAmount() {
        Order order = createOrder(50);  // 小订单
        User user = createUser(VipLevel.PLATINUM);  // 10%折扣=5
        // 假设有个大额优惠券
        Coupon coupon = createCoupon(CouponType.FIXED, "100", true);

        BigDecimal discount = calculator.calculateDiscount(order, user, coupon);

        // 折扣应该被限制为订单金额
        assertThat(discount).isEqualByComparingTo("50");
    }

    @Test
    @DisplayName("零元订单")
    void testZeroAmountOrder() {
        Order order = createOrder(0);
        User user = createUser(VipLevel.PLATINUM);
        Coupon coupon = createCoupon(CouponType.FIXED, "10", true);

        BigDecimal discount = calculator.calculateDiscount(order, user, coupon);

        assertThat(discount).isEqualByComparingTo("0");
    }

    // ==================== 组合场景测试 ====================

    @Test
    @DisplayName("多种折扣叠加:白金会员 + 满减 + 优惠券")
    void testCombinedDiscounts() {
        Order order = createOrder(300);  // 满足满减
        User user = createUser(VipLevel.PLATINUM);  // 10% = 30
        Coupon coupon = createCoupon(CouponType.FIXED, "15", true);  // 15元

        // 预期:30(会员) + 20(满减) + 15(优惠券) = 65
        BigDecimal discount = calculator.calculateDiscount(order, user, coupon);

        assertThat(discount).isEqualByComparingTo("65");
    }

    // ==================== 辅助方法 ====================

    private Order createOrder(int amount) {
        return createOrder(BigDecimal.valueOf(amount));
    }

    private Order createOrder(BigDecimal amount) {
        Order order = new Order();
        order.setAmount(amount);
        return order;
    }

    private User createUser(VipLevel level) {
        User user = new User();
        user.setLevel(level);
        return user;
    }

    private Coupon createCoupon(CouponType type, String value, boolean valid) {
        Coupon coupon = new Coupon();
        coupon.setType(type);
        coupon.setValue(new BigDecimal(value));
        coupon.setValid(valid);
        return coupon;
    }
}

第三步:审查测试质量

请检查这些测试用例是否有遗漏,并给出改进建议。

AI 审查并补充:

遗漏的测试场景

  1. 负数金额处理
java
@Test
@DisplayName("负数金额应该抛出异常或返回0")
void testNegativeAmount() {
    Order order = createOrder(-100);
    // 根据业务需求决定期望行为
}
  1. 超大金额(溢出风险)
java
@Test
void testVeryLargeAmount() {
    Order order = createOrder(new BigDecimal("999999999999"));
    // 验证不会溢出
}
  1. 并发安全(如果方法有状态)

改进建议

  1. 添加 @DisplayName 让测试报告更易读
  2. 使用 Builder 模式简化测试数据创建
  3. 考虑使用 Property-Based Testing 发现边界

结果对比

指标传统方式AI思维
用例设计30分钟,可能遗漏5分钟,系统全面
代码编写1-2小时10分钟生成
测试覆盖70-80%95%+
维护成本高(代码冗余)低(参数化测试)

关键洞察

  1. 先设计后编码:让 AI 先列出测试场景,避免遗漏
  2. 参数化测试:减少重复代码,提高可维护性
  3. 边界值分析:AI 更擅长系统性地找边界情况
  4. 审查补充:生成后让 AI 再审查一遍

测试生成 Prompt 模板

请帮我为这个函数生成单元测试。

## 代码
[粘贴待测试代码]

## 要求
- 使用 [JUnit 5/Jest/pytest] + [AssertJ/expect]
- 使用参数化测试减少重复
- 覆盖所有边界情况
- 包含正常流程和异常流程

请先列出测试场景矩阵,然后生成测试代码。

Released under the MIT License.