一、背景
smart-auto工具经过多年的迭代和好几代测试同学的开发,现在功能已经非常强大,支持各种HSF接口调用返回值的对比和断言能力,每日跑的测试件已经占到了淘宝买菜所有自动化测试件的55%以上。但是随着功能的不断增加,代码也越来越来庞大,之前单一的功能也变得复杂,代码的可维护性也在不停的降低。所以针对smart-auto接口测试相关的核心代码进行了一次重构,使代码变得更清晰和可维护。
二、现状分析
可以看下优化之前接口测试相关的核心代码,可以由下面简单的表述下这段代码意思:
1.工具中针对,Hsf接口校验共有四种方式HsfCheck1、HsfCheck2、HsfCheck3、HsfCheck4;
2.所有的接口校验方式都包含在一个Handler中,且不同的方式之间全部通过各种复杂的if-else分支来判断;
3.整体代码大概有200多行;
public CheckOutputModel doHandle(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel) throws Exception { if(!jsonPathList.isEmpty()){ if (attributeModel.isCompare()) { HsfCheck1 } }else{ if(attributeModel.isCompare()) { HsfCheck2 } } if ( checkConfigStatusObject == null ||checkConfigStatusObject.isEmpty() || checkConfigStatusObject.getBoolean("isCheck") == false ){ return result; }else{ if(assertExpectStr == null || !node.isCustom()){ HsfCheck3 }else{ HsfCheck4 } } }
完整的代码如下:
@Service("commonCheckHandlerAbandon") public class CommonCheckHandler implements CheckHandler{ @Resource private CaseConfigHandlerService caseConfigHandlerService; @Resource private CheckDataCaseService checkDataCaseService; private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public CheckOutputModel doHandle(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel) throws Exception { ThubNodeConfig node = JSON.parseObject(checkRecordModel.getTestsuiteDO().getStepConfig(), ThubNodeConfig.class); TestsuiteAttributeModel attributeModel = JSON.parseObject(checkRecordModel.getAttributes(), TestsuiteAttributeModel.class); if (checkRecordModel.getTestsuiteDO().getShadow()) { // 全链路压测标 EagleEye.putUserData("t", "1"); } CheckOutputModel result = new CheckOutputModel(); if(node==null){ result.setSuccess(false); return result; } List<String> jsonPathList = Collections.emptyList(); if(node.getJsonPath() != null && !node.getJsonPath().trim().isEmpty() ){ jsonPathList = Arrays.asList(node.getJsonPath().split(";")); } try{ //如果jsonPathList不为空,则执行jsonPath解析,执行jsonPath之后的对比 if(!jsonPathList.isEmpty()){ List<CheckDiffModel> totalFailInfo = new ArrayList<>(); List<String> errorMessages = new ArrayList<>(); // 用于存储错误消息 for (String jsonPath : jsonPathList) { try { if (attributeModel.isCompare()) { String actualResultStr = StringEscapeUtils.unescapeJavaScript((String) executeResultModel.getValueByKey(CheckCaseInfoConst.ACTUAL_RESULT)); String expectResultStr = StringEscapeUtils.unescapeJavaScript((String) executeResultModel.getValueByKey(CheckCaseInfoConst.EXPECT_RESULT)); Object actualResult = null; Object expectResult = null; if (StringUtils.isNoneBlank(actualResultStr)) { Object actualValueObject = JsonPath.read(actualResultStr, jsonPath); String actualValue = JSON.toJSONString(actualValueObject); if (JSON.isValidObject(actualValue)) { actualResult = JSON.parseObject(actualValue); } else if (JSON.isValidArray(actualValue)) { actualResult = JSON.parseArray(actualValue); } else { actualResult = JSON.parse(actualValue); } } if (StringUtils.isNoneBlank(expectResultStr)) { Object expectValueObject = JsonPath.read(expectResultStr, jsonPath); String expectValue = JSON.toJSONString(expectValueObject); if (JSON.isValidObject(expectValue)) { expectResult = JSON.parseObject(expectValue); } else if (JSON.isValidArray(expectValue)) { expectResult = JSON.parseArray(expectValue); } else { expectResult = JSON.parse(expectValue); } } StringBuffer ignorBuffer = new StringBuffer(); ignorBuffer.append(node.getIgnorConfig()); List<CheckDiffModel> failInfo = QAssert.getReflectionDiffInfo("assert diff", expectResult, actualResult, ignorBuffer.toString(), ReflectionComparatorMode.LENIENT_ORDER, ReflectionComparatorMode.LENIENT_DATES, ReflectionComparatorMode.IGNORE_DEFAULTS); failInfo.forEach(i -> i.setNodeName(jsonPath + "---" + i.getNodeName())); totalFailInfo.addAll(failInfo); } } catch (Exception e) { // 记录错误消息 String errorMessage = "Error with JSON path: " + jsonPath + " - " + e.getMessage(); errorMessages.add(errorMessage); logger.error(errorMessage, e); } } if (!totalFailInfo.isEmpty()||!errorMessages.isEmpty()) { if(!totalFailInfo.isEmpty()){ errorMessages.add(0, "value not same"); } // 组合错误消息,用回车符分隔 String combinedErrorMessages = String.join("\n", errorMessages); result.setSuccess(false); result.setErrorCode(combinedErrorMessages); result.setFailInfoList(totalFailInfo); } else { result.setSuccess(true); } //如果jsonPathList为空,走正常对比逻辑 }else { result.setTraceId(EagleEye.getTraceId()); if(attributeModel.isCompare()) { String actualResultStr = StringEscapeUtils.unescapeJavaScript((String) executeResultModel.getValueByKey(CheckCaseInfoConst.ACTUAL_RESULT)); String expectResultStr = StringEscapeUtils.unescapeJavaScript((String) executeResultModel.getValueByKey(CheckCaseInfoConst.EXPECT_RESULT)); Object actualResult = null; Object expectResult = null; if (StringUtils.isNoneBlank(actualResultStr)) { if (JSON.isValidObject(actualResultStr)) { actualResult = JSON.parseObject(actualResultStr); } else if (JSON.isValidArray(actualResultStr)) { actualResult = JSON.parseArray(actualResultStr); } else { actualResult = JSON.parse(actualResultStr); } } if (StringUtils.isNoneBlank(expectResultStr)) { if (JSON.isValidObject(expectResultStr)) { expectResult = JSON.parseObject(expectResultStr); } else if (JSON.isValidArray(expectResultStr)) { expectResult = JSON.parseArray(expectResultStr); } else { expectResult = JSON.parse(expectResultStr); } } StringBuffer ignorBuffer = new StringBuffer(); ignorBuffer.append(node.getIgnorConfig()); List<CheckDiffModel> failInfo = QAssert.getReflectionDiffInfo("assert diff", expectResult, actualResult, ignorBuffer.toString(), ReflectionComparatorMode.LENIENT_ORDER, ReflectionComparatorMode.LENIENT_DATES, ReflectionComparatorMode.IGNORE_DEFAULTS); if (!failInfo.isEmpty()) { result.setSuccess(false); result.setErrorCode("value not same"); result.setFailInfoList(failInfo); } else { result.setSuccess(true); } } } //执行断言校验 JSONObject checkConfigStatusObject = JSON.parseObject(checkRecordModel.getTestsuiteDO().getCheckConfigStatus()); //无断言直接返回 if ( checkConfigStatusObject == null ||checkConfigStatusObject.isEmpty() || checkConfigStatusObject.getBoolean("isCheck") == false ){ return result; }else{ //执行断言校验 String assertActualStr = StringEscapeUtils.unescapeJavaScript((String)executeResultModel.getValueByKey(CheckCaseInfoConst.ACTUAL_RESULT)); String assertExpectStr = StringEscapeUtils.unescapeJavaScript((String)executeResultModel.getValueByKey(CheckCaseInfoConst.EXPECT_RESULT)); CheckDataCaseDO checkDataCaseDO = caseParam.getCaseDO(); //断言对比 if(assertExpectStr == null || !node.isCustom()){ boolean checkResult = caseConfigHandlerService.resultAssert(checkRecordModel.getTestsuiteDO(), checkDataCaseDO,assertActualStr); if (!checkResult){ result.setSuccess(false); return result; } CheckDataCaseQueryDO checkDataCaseQueryDO = new CheckDataCaseQueryDO(); checkDataCaseQueryDO.setId(checkDataCaseDO.getId()); List<CheckCaseResult> checkResultList = checkDataCaseService.queryCheckCaseResult(checkDataCaseQueryDO).getResult(); List<CheckDiffModel> checkCoonfigFailInfo = new ArrayList<>(); for (CheckCaseResult checkCaseResult : checkResultList) { String checkConfigResult = checkCaseResult.getCheckConfigResult(); List<Map> checkParse = JSONArray.parseArray(checkConfigResult, Map.class); for (Map map : checkParse) { CheckDiffModel checkDiffModel = new CheckDiffModel(); String checkConfig = String.valueOf(map.get("checkConfigResult")); StringBuffer stringBuffer = new StringBuffer(); if(!StringUtils.equals(checkConfig,"true")){ stringBuffer.append((String)map.get("assertNode")+map.get("assertCondition")+map.get("assertErpect")); checkDiffModel.setActualValue("false"); checkDiffModel.setNodeName(String.valueOf(stringBuffer)); checkCoonfigFailInfo.add(checkDiffModel); } } } if (checkCoonfigFailInfo.size() != 0) { result.setSuccess(false); result.setErrorCode("value not same"); result.setFailInfoList(checkCoonfigFailInfo); } else{ result.setSuccess(true); } //跨应用断言校验 }else { boolean checkResult = caseConfigHandlerService.resultAssertComp(checkRecordModel.getTestsuiteDO(), checkDataCaseDO, assertActualStr,assertExpectStr); if (!checkResult){ result.setSuccess(false); return result; } CheckDataCaseQueryDO checkDataCaseQueryDO = new CheckDataCaseQueryDO(); checkDataCaseQueryDO.setId(checkDataCaseDO.getId()); List<CheckCaseResult> checkResultList = checkDataCaseService.queryCheckCaseResult(checkDataCaseQueryDO).getResult(); List<CheckDiffModel> checkCoonfigFailInfo = new ArrayList<>(); for (CheckCaseResult checkCaseResult : checkResultList) { String checkConfigResult = checkCaseResult.getCheckConfigResult(); List<Map> checkParse = JSONArray.parseArray(checkConfigResult, Map.class); CheckDiffModel checkDiffModel = new CheckDiffModel(); StringBuffer stringBuffer = new StringBuffer(); for (Map map : checkParse) { Boolean checkConfig = (Boolean) map.get("checkConfigResult"); if(!checkConfig){ stringBuffer.append((String)map.get("assertNode")+map.get("assertCondition")+map.get("assertErpect")); stringBuffer.append(","); checkDiffModel.setActualValue("false"); } } checkDiffModel.setNodeName(String.valueOf(stringBuffer)); checkCoonfigFailInfo.add(checkDiffModel); } if (checkCoonfigFailInfo.get(0).getActualValue() != null) { result.setSuccess(false); result.setErrorCode("value not same"); result.setFailInfoList(checkCoonfigFailInfo); } else{ result.setSuccess(true); } } } }catch(Exception e){ e.printStackTrace(); result.setSuccess(false); result.setMsgInfo(e.getMessage()); }finally{ EagleEye.removeUserData("t"); } return result; } }
以上代码有以下几点问题:
1.这段代码是由冗长的if-else分支判断组合起来的,且if-else的逻辑也比较混乱,然后这段代码把4种Hsf的接口检查都耦合在了一起,没有扩展性。后续增加任何功能,都需要在原来耦合的代码里添加代码,有可能会影响原有功能。
2.这段代码没有做到开闭原则,一段良好的代码需要做到对扩展开发,对修改关闭。
3.所有实现Hsf校验的逻辑都在一个handler类中,导致这个类中的代码很多,从而影响了代码的可读性、可维护性。
4.这段代码的if-else条件判断很难懂,无法判断某个条件中的校验到底是校验哪一种Hsf校验类型,每次查看这段代码都要研究好久。
三、解决方案
可以使用策略工厂模式来解决以上问题,把每种Hsf校验的方式封装起来,然后通过策略工厂模式来路由下发,把冗长的代码解耦出来,形成了一套框架,并且保证了代码的扩展性。废话不多说,直接看代码。
先构建一个策略工厂类:
public class CheckStrategyFactory { private final Map<CheckStrategySelector, HsfInterfaceCheck> strategyRegistry = new HashMap<>(); @Autowired public CheckStrategyFactory(HsfAssertCheck hsfAssertCheck, HsfCrossInterfaceAssertCompare hsfCrossInterfaceAssertCompare, HsfFullCompareCheck hsfFullCompareCheck, HsfMultipleJsonPathCompareCheck hsfMultipleJsonPathCompareCheck, JsonPathCompareStrategySelector jsonPathCompareStrategySelector, CrossInterfaceAssertCompareStrategySelector crossInterfaceAssertCompareStrategySelector, FullCompareStrategySelector fullCompareStrategySelector, AssertStrategySelector assertStrategySelector) { // 在构造函数或初始化块中注册所有策略 strategyRegistry.put(assertStrategySelector, hsfAssertCheck); strategyRegistry.put(crossInterfaceAssertCompareStrategySelector, hsfCrossInterfaceAssertCompare); strategyRegistry.put(fullCompareStrategySelector, hsfFullCompareCheck); strategyRegistry.put(jsonPathCompareStrategySelector, hsfMultipleJsonPathCompareCheck); // ... 注册更多策略 ... } public HsfInterfaceCheck getStrategy(ThubNodeConfig node, JSONObject checkConfigStatusObject,TestsuiteAttributeModel attributeModel , ExecuteResultModel executeResultModel) { for (Map.Entry<CheckStrategySelector, HsfInterfaceCheck> entry : strategyRegistry.entrySet()) { if (entry.getKey().matches(node, checkConfigStatusObject, attributeModel, executeResultModel)) { return entry.getValue(); } } return null; // 兜底检查策略返回null } }
再创建2个接口,一个策略选择接口CheckStrategySelector,一个Hsf校验接口HsfInterfaceCheck。
public interface CheckStrategySelector { boolean matches(ThubNodeConfig node, JSONObject checkConfigStatusObject , TestsuiteAttributeModel attributeModel , ExecuteResultModel executeResultModel); }
public interface HsfInterfaceCheck { CheckOutputModel check(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel); }
再创建4个策略类和4个Hsf校验类分别实现策略选择接口CheckStrategySelector和Hsf校验接口HsfInterfaceCheck。
以下是HsfCheck1和HsfCheck2策略选择类,省略其他2个。
public class AssertStrategySelector implements CheckStrategySelector { @Override public boolean matches(ThubNodeConfig node, JSONObject checkConfigStatusObject, TestsuiteAttributeModel attributeModel , ExecuteResultModel executeResultModel) { String assertExpectStr = StringEscapeUtils.unescapeJavaScript((String)executeResultModel.getValueByKey(CheckCaseInfoConst.EXPECT_RESULT)); return !(checkConfigStatusObject == null ||checkConfigStatusObject.isEmpty() || checkConfigStatusObject.getBoolean("isCheck") == false) && (assertExpectStr == null || !node.isCustom()); } }
public class FullCompareStrategySelector implements CheckStrategySelector { @Override public boolean matches(ThubNodeConfig node, JSONObject checkConfigStatusObject, TestsuiteAttributeModel attributeModel , ExecuteResultModel executeResultModel) { return attributeModel.isCompare() && (node.getJsonPath() == null || node.getJsonPath().trim().isEmpty()); } }
以下是HsfCheck1和HsfCheck2校验类,省略其他2个。
@Service("hsfAssertCheck") public class HsfAssertCheck implements HsfInterfaceCheck { @Resource private CaseConfigHandlerService caseConfigHandlerService; @Resource private CheckDataCaseService checkDataCaseService; @Override public CheckOutputModel check(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel) { }
@Service("hsfFullCompareCheck") public class HsfFullCompareCheck implements HsfInterfaceCheck { @Override public CheckOutputModel check(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel) { } }
最后Handler代码改造成了这一段。
@Service("commonCheckHandler") public class CommonCheckHandler implements CheckHandler{ private final CheckStrategyFactory factory; public CommonCheckHandler(CheckStrategyFactory factory) { this.factory = factory; } @Override public CheckOutputModel doHandle(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel) throws Exception { ThubNodeConfig node = JSON.parseObject(checkRecordModel.getTestsuiteDO().getStepConfig(), ThubNodeConfig.class); TestsuiteAttributeModel attributeModel = JSON.parseObject(checkRecordModel.getAttributes(), TestsuiteAttributeModel.class); JSONObject checkConfigStatusObject = JSON.parseObject(checkRecordModel.getTestsuiteDO().getCheckConfigStatus()); CheckOutputModel result = new CheckOutputModel(); if(node==null){ result.setSuccess(false); return result; } HsfInterfaceCheck hsfInterfaceCheckStrategy = factory.getStrategy(node, checkConfigStatusObject, attributeModel, executeResultModel); if(hsfInterfaceCheckStrategy != null){ return hsfInterfaceCheckStrategy.check(caseParam, checkRecordModel, executeResultModel); } else { result.setSuccess(false); result.setErrorCode("未找到对应的校验策略"); return result; } } }
以上通过策略工厂模式把那段代码拆成了多个文件,通过策略工厂模式把冗长的if-else代码给分解了,我们再来看一下重构后的代码是不是更好呢。下图展示了重构的整体逻辑:
重构之后,创建了工厂类,由工厂类中的策略判断逻辑来决定是哪一种策略类型,在运动时动态确定使用哪种策略,最终路由到对应的校验方法里。
1.最终代码实现了以下几个点:重构后的代码符合了开闭原则,添加新策略的时候,最小化、集中化代码改动、减少引入bug的风险。
2.重构后的代码解耦了之前代码的复杂度,解耦了策略的定义、创建和使用,控制代码复杂度,让每个部分的代码不至于太复杂、代码量过多。现在每个类的代码基本上在一显示屏就能展示完成。
3.大大增加了代码的可读性和可维护性。
四、总结
当然,并不是所有if-else分支都是烂代码,只要if-else分支不复杂,代码不多,这并没有问题,只要遵循KISS原则,怎么简单怎么来,就是最好的设计。但是一旦if-else分支很多,且每个分支都包含很多复杂的逻辑判断,这个时候就可以考虑是不是通过策略模式可以更清晰的梳理代码,使得代码维护性更强。
来源 | 阿里云开发者公众号
作者 | 汪峰(蔚风)