阿里妹导读
作者针对smart-auto接口测试相关的核心代码进行了一次重构,使代码变得更清晰和可维护。
一、背景
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")publicclass CommonCheckHandler implements CheckHandler{@Resourceprivate CaseConfigHandlerService caseConfigHandlerService;@Resourceprivate CheckDataCaseService checkDataCaseService;private Logger logger = LoggerFactory.getLogger(this.getClass());@Overridepublic 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); } elseif (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); } elseif (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); } elseif (JSON.isValidArray(actualResultStr)) { actualResult = JSON.parseArray(actualResultStr); } else { actualResult = JSON.parse(actualResultStr); } }if (StringUtils.isNoneBlank(expectResultStr)) {if (JSON.isValidObject(expectResultStr)) { expectResult = JSON.parseObject(expectResultStr); } elseif (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校验的方式封装起来,然后通过策略工厂模式来路由下发,把冗长的代码解耦出来,形成了一套框架,并且保证了代码的扩展性。废话不多说,直接看代码。
先构建一个策略工厂类:
publicclassCheckStrategyFactory{privatefinal Map<CheckStrategySelector, HsfInterfaceCheck> strategyRegistry = new HashMap<>();@AutowiredpublicCheckStrategyFactory(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(); } }returnnull; // 兜底检查策略返回null }}
再创建2个接口,一个策略选择接口CheckStrategySelector,一个Hsf校验接口HsfInterfaceCheck。
publicinterfaceCheckStrategySelector{booleanmatches(ThubNodeConfig node, JSONObject checkConfigStatusObject , TestsuiteAttributeModel attributeModel , ExecuteResultModel executeResultModel);}
publicinterfaceHsfInterfaceCheck {CheckOutputModel check(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel);}
再创建4个策略类和4个Hsf校验类分别实现策略选择接口CheckStrategySelector和Hsf校验接口HsfInterfaceCheck。
以下是HsfCheck1和HsfCheck2策略选择类,省略其他2个。
publicclassAssertStrategySelectorimplementsCheckStrategySelector{@Overridepublicbooleanmatches(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()); }}
publicclassFullCompareStrategySelectorimplementsCheckStrategySelector{@Overridepublicbooleanmatches(ThubNodeConfig node, JSONObject checkConfigStatusObject, TestsuiteAttributeModel attributeModel , ExecuteResultModel executeResultModel){return attributeModel.isCompare() && (node.getJsonPath() == null || node.getJsonPath().trim().isEmpty()); }}
以下是HsfCheck1和HsfCheck2校验类,省略其他2个。
@Service("hsfAssertCheck")publicclassHsfAssertCheckimplementsHsfInterfaceCheck{@Resourceprivate CaseConfigHandlerService caseConfigHandlerService;@Resourceprivate CheckDataCaseService checkDataCaseService;@Overridepublic CheckOutputModel check(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel){}
@Service("hsfFullCompareCheck")publicclassHsfFullCompareCheckimplementsHsfInterfaceCheck{@Overridepublic CheckOutputModel check(CheckCaseModel caseParam, BuildTestsuiteModel checkRecordModel, ExecuteResultModel executeResultModel){ }}
最后Handler代码改造成了这一段。
@Service("commonCheckHandler")publicclassCommonCheckHandlerimplementsCheckHandler{privatefinal CheckStrategyFactory factory;public CommonCheckHandler(CheckStrategyFactory factory) {this.factory = factory; }@Overridepublic 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分支很多,且每个分支都包含很多复杂的逻辑判断,这个时候就可以考虑是不是通过策略模式可以更清晰的梳理代码,使得代码维护性更强。
继续阅读
阅读原文