写单元测试
问题描述
有一个复杂的订单折扣计算函数,需要覆盖各种边界情况。手动想测试用例容易遗漏,而且写测试代码很繁琐。
java
public BigDecimal calculateDiscount(Order order, User user, Coupon coupon) {
// 复杂的折扣计算逻辑
// 涉及:会员等级、订单金额、优惠券类型、商品类目等
}传统思路
- 看代码想用例 - 容易遗漏边界情况
- 复制粘贴改参数 - 测试代码冗长重复
- 只测正常流程 - 覆盖率不够
- 临时加用例 - 出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 审查并补充:
遗漏的测试场景
- 负数金额处理
java@Test @DisplayName("负数金额应该抛出异常或返回0") void testNegativeAmount() { Order order = createOrder(-100); // 根据业务需求决定期望行为 }
- 超大金额(溢出风险)
java@Test void testVeryLargeAmount() { Order order = createOrder(new BigDecimal("999999999999")); // 验证不会溢出 }
- 并发安全(如果方法有状态)
改进建议
- 添加
@DisplayName让测试报告更易读- 使用 Builder 模式简化测试数据创建
- 考虑使用 Property-Based Testing 发现边界
结果对比
| 指标 | 传统方式 | AI思维 |
|---|---|---|
| 用例设计 | 30分钟,可能遗漏 | 5分钟,系统全面 |
| 代码编写 | 1-2小时 | 10分钟生成 |
| 测试覆盖 | 70-80% | 95%+ |
| 维护成本 | 高(代码冗余) | 低(参数化测试) |
关键洞察
- 先设计后编码:让 AI 先列出测试场景,避免遗漏
- 参数化测试:减少重复代码,提高可维护性
- 边界值分析:AI 更擅长系统性地找边界情况
- 审查补充:生成后让 AI 再审查一遍
测试生成 Prompt 模板
请帮我为这个函数生成单元测试。
## 代码
[粘贴待测试代码]
## 要求
- 使用 [JUnit 5/Jest/pytest] + [AssertJ/expect]
- 使用参数化测试减少重复
- 覆盖所有边界情况
- 包含正常流程和异常流程
请先列出测试场景矩阵,然后生成测试代码。