关于单元测试最佳实践的两个问题

关于单元测试的两个问题请教

问题一

业务

下单页查询优惠券

代码

    // 下单页查询可用优惠券
    public List<Coupon> getAvailableCoupons(String productCategory, int productPrice, String shopId, String userId){
        // 查询所有可用优惠券
        List<Coupon> couponList = couponMapper.queryCoupons(userId);
        // 筛选优惠券
        List<Coupon> availableList = new ArrayList<>();
        for (Coupon coupon : couponList) {
            // 校验类目券 如图书券不能用于购买手机
            if(!checkCategory(coupon,productCategory)){ // 类目不匹配
                continue;
            }
            // 校验满减券 如满100减50
            if(!checkPrice(coupon,productPrice)){ // 作品金额不足满减金额
                continue;
            }
            // 校验店铺券
            if(!checkShopCoupon(coupon,shopId)){ // 排除其他店铺的优惠券
                continue;
            }
            availableList.add(coupon);
        }
        return availableList;
    }

单元测试代码

    @Test
    public void test_exclude_coupon_which_belong_to_other_shop(){
        // 测试排除了其他店铺券
        String productCategory = "book";
        int productPrice = 200;
        String shopId = RandomStringUtils.randomAlphanumeric(10); //作品所属店铺
        String userId = RandomStringUtils.randomAlphanumeric(10);

        Coupon coupon = new Coupon();
        String anotherShopId = RandomStringUtils.randomAlphanumeric(10);
        coupon.setShopId(anotherShopId); // 其他店铺的优惠券
        coupon.setCategory("book"); // 图书券
        coupon.setFullPrice(200); // 满200减50
        coupon.setPrice(50);

        when(couponMapper.queryCoupons(userId)).thenReturn(newArrayList(coupon));

        List<Coupon> availableCoupons = service.getAvailableCoupons(productCategory, productPrice, shopId, userId);
        // 验证 是否排除了其他店铺的优惠券
        assertEquals(0, availableCoupons.size());
    }

虽然单元测试执行通过了 但是仍没有信心 因为不知是否确实调用了checkShopCoupon 还是直接在前面的校验就失败了 如checkCategory永远都返回false 因为(这些校验)是内部私有方法 没办法通过verify来判断是否调用了

另外每种校验也都有好几种情况 如

校验类目
- 如果是通用券 不限类目 如category_all 返回true
- 否则优惠券的类目列表不含作品类目 返回false

校验金额
- 如果是无门槛券 直接返回true
- 如果是满减券 校验作品金额 是否 大于大于等于 满减金额

是不是单独测试这些校验方法就可以了 如果这样的话 需要将这些私有方法变成protected包私有

问题二

业务

下单页查询可用优惠券

  • 查询所有可用优惠券

  • 筛选

  • 组装返回Vo

对应的方法为

ResultVo getAvailableCouponsWhenOrder(...){
    List<Coupon> couponList = queryAll(userId);
    List<Coupon> availableList = filterCoupons(couponList,...);
    ResultVo result = buildResultVo(availableList,...);
    return result;
}

现在单元测试只想测试筛选优惠券 即filterCoupons 但是它是一个私有方法 那么只好把它改成包私有或者protected

阅读 3.5k
2 个回答
问题1:
虽然单元测试执行通过了 但是仍没有信心 因为不知是否确实调用了checkShopCoupon 还是直接在前面的校验就失败了 如checkCategory永远都返回false 因为(这些校验)是内部私有方法 没办法通过verify来判断是否调用了

没有信心的一个原因是测试场景覆盖不全,另一个在缺乏策略,在复杂的控制逻辑中如果要测试某个方法被call过,而该方法是私有的,有一个包可以试一下叫powermock,其中mock私有方法可以成功。很强大,不过在某种程度上,使用到powermock说明,我们写的代码可测试性很差,这是一个问题。
问题二
用上一个方法也可以解决。

建议使用TDD,如果资源自由的情况下,帮助更好思考业务,并且让实现的功能代码天生可测试。

一种解决方案 使用spel表达式来配置优惠券筛选规则 如下所示

  • 校验类目规则 不是通用券且不等于商品类目 排除 category != 'all' and category != productCategory

  • 校验满减金额规则 满减券且订单金额不足满减金额 排除 fullPrice != null and orderPrice < fullPrice

  • 校验店铺券规则 店铺券且不同于作品的店铺 couponFrom == 'shop' and couponShopId != productShopId

这些规则 可以配置在数据库中 多个的话 可以用逗号分隔

代码为

        // 筛选优惠券
        List<Coupon> availableList = new ArrayList<>();
        // 从数据库查询出优惠券过滤规则配置
        String rules = couponMapper.getFilterRules();
        List<String> ruleList = asList(rules.split(","));
        outer:for (Coupon coupon : couponList) {
            for (String rule : ruleList) {
                // 遍历规则 一旦满足 就排除
                Expression expression = expressionParser.parseExpression(rule);
                Boolean value = expression.getValue(coupon, Boolean.class);
                if(value){
                    continue outer;
                }
            }
            availableList.add(coupon);
        }

若只想测试校验类目规则 可以如下处理

// coupon1 通用券 coupon2 类目券等于商品类目 coupon3 类目券且不同于商品类目【被排除】
when(couponMapper.queryCoupons(userId)).thenReturn(newArrayList(coupon1,coupon2,coupon3));
// 返回的规则中 只有类目筛选规则
when(couponMapper.getFilterRules()).thenReturn("category != 'all' and category != productCategory");
// 验证应该返回2个优惠券
assertEquals(2, availableList.size()); 
// 校验ID应该是1,2

其他规则也可以同样的方式处理

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进