導讀
作者針對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")
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分支很多,且每個分支都包含很多複雜的邏輯判斷,這個時候就可以考慮是不是通過政策模式可以更清晰的梳理代碼,使得代碼維護性更強。
作者:汪峰(蔚風)
來源-微信公衆号:阿裡雲開發者
出處:https://mp.weixin.qq.com/s/tg4vTL6_TI-tnxaMyVLhsA