前言
一直以來,作為網際網路軟體工程師接觸最多的事務之一便是持續整合(Continuous integration,簡稱 CI)。持續整合儼然已成為主流網際網路軟體開發流程中一個重要的環節。現今有贊內部在實踐持續交付(Continuous delivery,簡稱 CD),它可以被看成是後持續整合時代的產物。需要強調的是,不管是 CI 還是 CD,更多的是強調作為軟體開發交付過程中的實踐,而一旦交付到生產環境 CI 和 CD 就無能為力了。有贊線上撥測系統正是為了彌補這一不足。現有的線上保障手段可分為運維層面、產品層面、安全層面、服務層面和測試層面等維度。本文重點介紹我們在測試層面的實踐。
基於測試指令碼的線上監控產生
我們做測試線上撥測系統的初衷有以下幾點:
- 主動預警線上問題。有贊有很多個業務線,各個業務線有不同的開發測試同學對接,我們很難做到每次釋出都把影響面評估得十分準確。運維層面的監控更多的是被動告警,即使用者流量觸發了線上 bug,我們才會收到報警,使用者體驗不夠好。我們需要線上上 bug 預警方面變被動為主動,週期性地知曉各個業務線的健康狀況。
- 小流量下敏捷發現線上問題。通常我們軟體的釋出都是在凌晨流量非常低的時候進行。釋出完成後,迴歸時間長(靠手動),測試面有限(無法做到次次釋出全量回歸)。此時需要敏捷構造一波覆蓋面全的流量,在小流量背景下,敏捷發現線上問題。
- 知曉緊急情況下業務的受影響範圍以及後續收斂情況。例如當生產環境出現網路異常等非軟體故障時,需要清楚業務層面的影響;當網路恢復後,需要知道業務影響是否都已經收斂。 在此之前這些場景都需要測試人員手工介入,靈活度敏捷度都非常差。有了這套系統後,測試人員可以增加自己關注的場景,場景可以通過主動觸發和定時觸發來執行,通過告警系統通知到有關人員,做到第一時間排查問題,減少故障影響,降低故障時長。
基礎版
1.0 版本我們使用通用的 SpringWeb 搭建,有贊內部稱為線上機器人檢查。系統結構如下
1.0 版系統架構圖
系統主要為三個模組:
- 任務排程模組。該模組將用例執行封裝成系統任務,使用 Spring Quartz 來定時觸發。對外提供 API 對接有贊釋出平臺,每當系統釋出上線完成後主動觸發用例執行。
- 測試用例模組。包括業務訪問,斷言和告警。測試場景需要各個業務線的測試同學投入開發。
- 告警模組。對接有贊內部告警平臺。
1.0版流程圖
系統將用例分為基礎用例和場景用例,支援場景併發或者順序同步執行。具體執行策略由用例設計者結合具體情況在用例開發過程中設定。
存在的問題
基礎版滿足了最小可用,這種方式優點在於前期能夠快速投入使用,且對於經常寫整合用例的人來說成本不高,但對其他人(測試新人、開發、運維等)則不然。概括而言,其缺點主要集中在以下幾點:
- 業務線一旦多起來,用例程式碼開發成本提高;
- 隨著用例數量增加,後期用例維護成本很大;
- 用例上線不靈活,每次用例改動需要重新發布;
- 無法直觀看到執行情況和業務覆蓋情況;
- 每次執行不區分業務,全量執行;
- 用例程式碼存在冗餘,效率比較低。
配置化和視覺化
由於這些不可規避的問題,我們重新設計併發布了 2.0 版本。對應解決以上問題:
- 測試用例和測試場景支援配置化,可以從管理平臺上配置;
- 用例配置標準化,給定標準用例結構和斷言策略;
- 通過管理平臺來管理自己的用例,用例改動實時生效,無需釋出;
- 增加前端展示,通過圖表直觀展示執行情況和業務覆蓋情況,方便不同人群查閱;
- 對接釋出平臺,按照指定的應用名來區分跑哪些用例;
- 設計用例執行框架,實現核心程式碼複用。
新版系統架構圖如下
2.0版系統架構圖
用例模型如下:
欄位 | 是否必填 | 說明 |
---|---|---|
用例名稱 | 是 | 建議命名格式:“用例型別:服務:方法” |
用例型別 | 是 | 兩種型別可選http或dubbo |
用例描述 | 是 | 場景描述 |
所屬業務 | 是 | 用例所屬業務閾 |
請求url | 否 | http協議呼叫的url |
請求頭 | 否 | http header |
請求引數 | 否 | http或dubbo的請求入參。支援動態引數注入實現用例間依賴 |
服務名稱 | 否 | 對應請求dubbo協議的介面名(包名+類名) |
請求方法 | 是 | http協議:GET、POST、PUT等;dubbo協議:方法名 |
斷言 | 是 | 支援多個 |
是否開啟 | 否 | 控制開關,關閉後不再運行。預設開啟 |
是否登入 | 否 | 開啟後,使用預設賬號進行登入操作。預設不開啟 |
是否重試 | 否 | 開啟後,⽤例失敗重試1次。預設否 |
前/後置檢查 | 否 | 執行⽤例前/後,先執行前/後置檢查,失敗則中斷 |
為了更直觀展示線上業務的健康狀況我們增加了豐富前端報表
資料展示
新版本與老版本的主要區別在於:
- 將執行流和資料流進行了分離,測試用例設計無需編碼,支援配置化,用例作為資料存放到 DB 中重複使用,用例的執行引擎管理用例的執行流。
- 對通用的事務進行了封裝,比如登入、切換店鋪等操作,通過統一的執行緒池進行管理。
- 支援動態引數注入,實現了用例間的相互依賴,後面再單獨介紹這塊內容。
任務執行流程圖如下:
2.0版流程圖
任務執行引擎通過不同的工作執行緒實現。不同業務用例併發執行,業務內部用例序列執行。系統根據不同的用例的型別(http/dubbo)分發到具體任務流中。
核心類設計
用例間依賴的實現
從用例的複雜度上講,我們的用例主要分為兩大類:單一場景的基礎用例和複雜場景的組合用例。組合用例是在基礎用例的基礎上進行一定的整合,用例的輸入輸出存在一定的依賴。我們實現用例依賴的方式有兩種:
- 通過配置用例的前置後置關係。
- 通過引數注入。
第一種方式,在配置用例的時候,給它一個前置用例,當然前置用例也是在平臺中管理的。這樣當執行到該用例的時候,執行引擎會先去執行前置用例。
第二種方式,針對 Json 格式的入參,我們定義如下格式進行引數注入:
$#a,b,c#$
各個欄位分別代表的含義為:
a:被依賴用例的ID
b:被依賴用例響應的欄位(key值),比如:name
c:可選欄位,當被依賴值位於 array 裡面時,取其 index 下標
舉例:{"code":"$#8,data,0#$","type":"$#10,type#$"}
引數注入的流程如下:
引數注入流程圖
斷言模組設計
在新版系統裡面,我們設計了四種型別的通用斷言,幾乎可以滿足我們自己的所有應用場景。這四種型別分別是:
-
是否包含。 響應內容包含指定內容為 true,反之為 false。
-
非空/null。 響應內容非空/null為 true,為空/null為 false。
-
JSON 特定位置的值的“相等”判斷。 這種情況系統首先會將響應內容轉換成 json,新增斷言時需要指定待比較物件在 json 串中的座標。如果該座標上的值與指定的值相等則為 true,反之為 false。 那麼如何給一個 json 串的每個值設定一個獨一無二的座標呢?考慮到 json 存在巢狀關係且 key 可能重複,我們通過一種複合 key 的來表示這個座標,例如有如下 json:
{
"data": {
"list": [
"1",
"2"
],
"info": {
"name": "張三",
"age": 18
}
},
"code": 200
}
對標紅的值的斷言可以這樣表示:{"data":{"info":{"name":"張三"}}}
,如果返回的位置的值為"張三"則判斷結果為 true,否則為 false。
- 面向 JSON 的虛擬碼表示式判斷
前面三種型別的斷言僅滿足了部分場景,對於一些複雜的斷言仍然無法滿足,比如上文 json 中 list size 的斷言。為此,我們引入第四種斷言方式---虛擬碼斷言。針對 list size 的斷言我們可以這樣寫:
程式碼在處理的時候會將該表示式拼接在 json 物件後進行執行。整段程式碼執行的結果為真斷言為 true,否則為 false。 虛擬碼的動態編譯、載入和呼叫,採用 GroovyShell 來實現。該部分程式碼實現如下:
public Result compare(String response) {
Result result = new Result();
// 單例獲取GroovyShell
GroovyShell shell = SingleGroovyUtil.getGroovyShell();
Binding binding = null;
JSONObject jsonObject = new JSONObject();
JSONArray jsonArray = new JSONArray();
Object value = null;
try {
if (response.startsWith("[")){
jsonArray = JSON.parseArray(response);
binding = new Binding();
binding.setVariable("data", jsonArray);
value = InvokerHelper.createScript(shell.getClass(), binding).evaluate("data." + textStatement);
}else {
jsonObject = JSON.parseObject(response);
binding = new Binding();
binding.setVariable("data", jsonObject);
value = InvokerHelper.createScript(shell.getClass(), binding).evaluate("data." + textStatement);
}
if((Boolean)value) {
result.setSuccess(true);
}else {
result.setSuccess(false);
String msg = JsonUtil.findErrMsgByJsonObject(jsonObject);
result.setMsg(String.format("斷言失敗。斷言的內容[%s], 錯誤描述[%s]", this.textStatement, msg.length()>0?msg:response));
}
} catch (Exception e) {
result.setSuccess(false);
String msg = JsonUtil.findErrMsgByJsonObject(jsonObject);
result.setMsg(String.format("斷言時發生異常。ErrMsg=[%s],actual=[%s]", e.getMessage(), msg.length()>0?msg:response));
} finally { // 處理完後,主動將物件置為null
binding = null;
}
return result;
}
複製程式碼
外掛化
新版系統滿足了用例的可配置化以及視覺化的要求,同時也犧牲了一部分的靈活性。例如一些複雜斷言的虛擬碼會非常長,且可讀性不高,一不留神就會出錯;簡單的用例依賴可以滿足,複雜的用例依賴卻很難滿足。比如用例 A 在某些條件下依賴用例 B,其他條件下依賴用例 C,這種複雜依賴關係走配置化並不合適。基於以上考慮,我們在現有的系統的基礎上又增加了外掛化的特性,來支援複雜用例的接入。
3.0 版系統架構圖
外掛化的設計思想如下:
- 平臺對外提供一套用例標準,測試同學開發符合標準的用例新增到平臺即可執行。
- 用例與平臺完全解耦,用例在平臺可配置。
- 用例支援熱插拔,平臺無需重啟。
用例標準通過介面的形式對外提供,封裝成jar包暴露出來。用例設計者直接依賴該jar包並實現指定介面即可。用例介面定義如下:
public interface AbstractTestCase {
CaseResult before();
CaseResult run();
void after();
}
複製程式碼
用例開發完成後打包成 jar 包上傳到平臺,一個 jar 包中可包含一個用例也可以包含多個用例。
jar 包上傳後平臺要做的事情如下:
- 動態把 jar load 進 JVM
- 解析實現了 AbstractTestCase 介面的類
- 按照指定策略呼叫類中的方法
- 上報並展示結果資料
獲取 jar 包中實現了 AbstractTestCase 介面的程式碼如下:
/**
* 獲取jar包中某介面的實現類
*/
public static List<Class<?>> getAllImplClassesByInterface(Class c) {
List<Class<?>> filteredList = new ArrayList<Class<?>>();
//判斷是否是介面
if (c.isInterface()) {
try {
//獲取jar包中的所有類
List<Class> allClass = getClassesByPackageName();
allClass.forEach(clazz -> {
if (c.isAssignableFrom(clazz)) {
if (!c.equals(clazz)) {
filteredList.add(clazz);
}
}
});
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return filteredList;
}
複製程式碼
未來
未來有贊線上撥測系統會提供更豐富的功能,例如更靈活的用例執行策略,核心用例執行頻率更高,邊緣業務執行頻率降低;更全面的報警策略,各個業務方可以自由定製關心的用例,線上問題第一時間觸達;支援多機房,目前該系統只在單機房進行部署,有贊核心業務已完成多機房部署,撥測系統也會隨之調整;系統支援分散式,為了防範系統單點故障,未來還會考慮進行分散式部署。
目前這套系統可以保障測試同學第一時間知曉有贊線上核心業務異常,將來保障的業務廣度和深度會進一步提高,成為有贊線上質量保障至關重要的一環。