及lambda表达式隔离和模拟外部依赖更容易滴单测” 一文已经初步探讨了如何使用函数接口及lambda表达式来隔离和模拟外部依赖,增强代码可测性。不过不彻底。 如果一个方法里含有多个外部服务调用怎么办? 如果方法A调用B,B调用C,C调用D,D依赖了外部服务,怎么让 A,B,C,D更加容易测试? 如何可配置化地调用外部服务,而让类的大部分方法保持函数纯粹性而容易单测,少部分方法则承担外部服务调用的职责?指导思想是: 通过函数接口隔离外部服务依赖,分离出真正可单测的部分 。真正可单测的部分往往是条件性、循环性的不含服务调用依赖的业务性逻辑,而顺序的含服务调用依赖的流程性逻辑,应当通过接口测试用例来验证。
表达与执行分离
表达通常是声明式的,无状态的;执行通常是命令式的,有状态且依赖外部环境的。 表达与执行分离,可将状态与依赖分离出来,从而对表达本身进行单测。来看一段代码:
public Deliverer getDeliverInstance(DeliveryContext deliveryContext, ExpressParam params) {
if (periodDeliverCondtion1) {
LogUtils.info(log, "periodDeliverer for {}", params);
return (Deliverer) applicationContext.getBean("periodDeliverer");
}
if(periodDeliverCondtion2){
LogUtils.info(log, "periodDeliverer for {}", params);
return (Deliverer) applicationContext.getBean("periodDeliverer");
}
if (fenxiaoDelivererCondition) {
LogUtils.info(log, "fenxiaoDeliverer for {}", params);
return (Deliverer) applicationContext.getBean("fenxiaoDeliverer");
}
if (giftDelivererCondition) {
LogUtils.info(log, "giftDeliverer for {}", params);
return (Deliverer) applicationContext.getBean("giftDeliverer");
}
if (localDelivererCondition) {
LogUtils.info(log, "localDeliverr for {}", JsonUtils.toJson(order));
return (Deliverer) applicationContext.getBean("localDeliverer");
}
LogUtils.info(log, "normalDeliverer for {}", params);
return (Deliverer) applicationContext.getBean("normalDeliverer");
}
这段代码根据不同条件,获取对应的发货子组件。 可见,代码要完成两个子功能: (1) 根据不同条件判断需要何种组件; (2) 获取相应组件,并打印必要日志。 (1) 是表达,真正值得测试的部分, (2) 是执行,通过接口测试即可验证; 而代码将(1)与(2) 混杂到一起,从而使得编写整个单测难度变大了,因为要mock applicationContext,还需要注入外部变量 log 。 可以将(1) 抽离出来,只返回要发货组件标识,更容易单测,而(2) 则使用多种方式实现。如下代码所示:
public Deliverer getDeliverInstanceBetter(DeliveryContext deliveryContext, ExpressParam params) {
return getActualDeliverInstance(getDeliverComponentID(deliveryContext, params).name(), params);
}
public DelivererEnum getDeliverComponentID(DeliveryContext deliveryContext, ExpressParam params) {
if (periodDeliverCondtion1) {
return periodDeliverer;
}
if(periodDeliverCondtion2){
return periodDeliverer;
}
if (fenxiaoDelivererCondition) {
return fenxiaoDeliverer;
}
if (giftDelivererCondition) {
return giftDeliverer;
}
if (localDelivererCondition) {
return localDeliverer;
}
return normalDeliverer;
}
public Deliverer getActualDeliverInstance(String componentName, ExpressParam params) {
LogUtils.info(log, "component {} for {}", componentName, params);
return (Deliverer) applicationContext.getBean(componentName);
}
public enum DelivererEnum {
normal
虽然多出了两个方法,但是只有 getDeliverComponentID 方法是最核心的最需要单测的,并且是无状态不依赖外部环境的,很容易编写单测,只需要测试各种条件即可。这里定义了 DelivererEnum ,是为了规范发货组件的名称仅限于指定的若干种,防止拼写错误。
识别业务逻辑中的表达与执行,将表达部分分离出来。
分离纯函数
看下面这段代码:
/**
* 根据指定rowkey列表及指定列族、列集合获取Hbase数据
* @param tableName hbase表名
* @param rowKeyList rowkey列表
* @param cfName 列族
* @param columns 列名
* @param allowNull 是否允许值为null,通常针对rowkey
* @return hbase 数据集
* @throws Exception 获取数据集失败时抛出异常
*/
public List<Result> getRows(Stri