51信用卡的微服務整合測試自動化探索
微服務架構下整合測試自動化的困境
先看下《微服務設計》1.3章節對SOA的定義:SOA(Service-Oriented Architecture,面向服務的架構)是一種設計方法,其中包含多個服務,而服務之間通過配合最終會提供一系列的功能。一個服務通常以獨立的形式存在於作業系統程序中。服務之間通過網路呼叫,而非採用程序內呼叫的方式進行通訊。微服務架構是SOA的一種特定方法,基於這種架構在開發層面帶來的好處在《微服務設計》一書中描述得很清楚了。從測試角度來看,通訊方式由程序內呼叫變成網路呼叫,最明顯的改變有兩個:
第一,資料流轉更加清晰。微服務架構下的時序圖非常清晰,服務之間分工明確,代替了傳統服務資料只在服務內部模組間流轉的方式。
第二,資料入口多元化,可測試性增強。服務的拆分帶來的另一個好處就是測試粒度變小變細,每個微服務都具備獨立的網路呼叫方式,不管是提供Http介面還是消費訊息佇列,都給整合測試可測試性和覆蓋率帶來了便利和提升。
整合測試自動化的困境
軟體工程沒有銀彈,對於微服務也是一樣。在《微服務設計》1.1.1章節做著Sam Newman對微服務架構的擔憂:當考慮(微服務)多小才足夠小的時候,我會考慮這些因素:服務越小,微服務架構的優點和缺點也就越明顯。使用的服務越小,獨立性帶來的好處就越多。但是管理大量服務也會越複雜,本書的剩餘部分會詳細討論這一複雜性。如果你能夠更好地處理這一複雜性,那麼就可以盡情地使用微服務了。一方面服務拆分給測試帶來很多便利,另一方面服務數量的增加也帶來了測試複雜度的指數增長,好像陷入了一種囚徒困境。
以下圖的功能為例,該功能一共涉及了7個微服務,2個訊息中介軟體,還有沒在時序圖上體現的兩種DB(MySQL/Cassandra)和快取中介軟體(Redis)。由於服務和中介軟體之間的呼叫也是網路呼叫,所以在測試過程可以把中介軟體和DB當成一個微服務節點來分析。面對這樣一個涉及12個微服務的功能(不考慮集測自動化策略,比如拆分成幾個小功能分塊實現自動化降低複雜度),我們需要一個什麼樣的框架來支撐,才能讓測試工程師寫出的集測自動化case具備易用性、可維護性這些美好的特性。
拆解困境
當時51信用卡的集測框架大概使用了一年多,中間迭代過兩個大的版本,已經從最原始的純Java框架過渡到基於關鍵字驅動和資料驅動開發的框架,原理參考下圖:
圖二
初代框架第一層是測試指令碼(參考下圖的程式碼)和Java Bean,第三層是工具層,由常用的Util類組成,比如:MysqlClientUtil、HttpClientUtil、RedisClientUtil等。第二層是解析層或者執行層,它會將第一層的測試指令碼初始化為對應的Java Bean例項,然後再把例項拆分成各個Util類的靜態方法執行。
//初代框架測試指令碼
{
"mysqlClient": {
"url": "mysqlurl:3306",
"username": "username",
"password": "password",
"sqlList": [
"insert into user (userid, username) values (123456, 'zhangsan')"
]
},
"httpClient": {
"reqeust": {
"method": "GET",
"url": "serice/queryUserInfo",
"headers": null,
"query": {
"userid": 123456
},
"body": null
},
"responseExpected": {
"headers": null
"body": {
"username": "zhangsan"
}
}
},
"redisClient": {
"address": "redisaddress:6307",
"command": "get userinfo:123456"
"resultExpected": {
"username": "zhangsan"
}
}
}
初代框架在面臨圖一複雜的功能時,最重要的一個問題就是擴充套件性。指令碼的每個modul都代表了一次網路呼叫,可讀性很好。但是測試流程被固化在每個Java Bean之中,每一個新的功能都有可能要定製一個新的Java Bean(調整action呼叫順序)和DataProvider(圖一 藍色部分)。而當Util類變更時,同樣需要變更Java Bean和DataProvider,這個變更的工作量堪稱地獄級。
第二個問題是資料的重用。圖二中的userid這個欄位在指令碼中出現3次,在某些複雜場景下可能就不是一個欄位而是整個資料結構,降低了case的可維護性。資料庫連線和中介軟體連線(指令碼中的redis client)每個case都會先初始化再銷燬,這個對於執行效率也是一種影響。
基礎框架有了,問題也找到了,下面我們要做的就是改造基礎框架來滿足新的自動化需求。
造一個“輪子”逃出困境
BDD is largely facilitated through the use of a simple domain-specific language (DSL) using natural language constructs (e.g., English-like sentences) that can express the behavior and the expected outcomes. Test scripts have long been a popular application of DSLs with varying degrees of sophistication. BDD is considered an effective technical practice especially when the "problem space" of the business problem to solve is complex.
——BDD在尋找擴充套件性解決方案的過程中,瞭解到兄弟部門正在嘗試用Rest-Assured代替初代框架中基於Apache HttpClient封裝的Util。通過了解Rest-Assured我們接觸到了BDD,其中領域驅動設計(domain-driven design)的idea讓我們沉思:是不是初代框架在設計模式上就有缺陷?
從元資料角度看,BDD中的behavior和關鍵字驅動是一個概念,關鍵字相對獨立和分散,BDD的優勢是通過領域驅動對behavior做了一層抽象,並通過一定的邏輯(given when then)將behavior串聯成一個具體的功能。這樣帶來的好處是,所有的behavior在當前領域只做一件事,比如在測試領域behavior就是做功能驗證(或者為功能驗證做準備),我們要做的就是針對測試領域的behavior抽象出一個父類,並通過一定規則將不同的behavior串聯起來。
從case指令碼的表現力來對比,資料驅動下的指令碼只做資料儲存,BDD使用DSL作為指令碼語言,可以表達出更豐富的行為和預期結果(express the behavior and the expected outcomes),可讀性和資訊的承載度上了一個臺階。這一part我們要做的就是選擇合適的語言作為框架的DSL。
通過對比BDD和關鍵字+資料驅動混合模式,我們得到了兩個action,後面要做的就是驗證action是否正確。
行為父類Behavior
測試的本質就是功能驗證,所有的行為不是為了功能驗證就是在為驗證做準備。舉個例子,測一個查詢介面,首先在DB中插入需要的資料,然後呼叫查詢介面,最後對比介面返回的實際資料是否和預期資料一致。在這個資料流轉過程中,插入DB的資料可能有部分和介面返回的資料是同一個,手工執行的過程中,這些資料會存在測試工程的腦海或者中間媒介,相當於執行緒中的上下文,而每個行為都有可能去操作這個上下文。
針對上文測試行為的兩個特徵,抽象了兩個介面CompareIntf和AssignContextIntf。CompareIntf負責功能驗證,返回值就是預期值和實際值的對比結果,如果是為了驗證做準備的行為,比如向DB插入資料,以行為的成功與否作為返回值。AssignContextIntf負責和上下文進行資料互動,包括增刪改查,而且只有當CompareIntf返回true的時候才會執行AssignContextIntf。
//測試行為介面抽象
public interface CompareIntf {
public boolean compareExpectAndActual();
}
public interface AssignContextIntf {
public void assignContext();
}
最終抽象出的父類參考下文UML類圖。Behavior類成員變數中的兩個Map以及相關的API先按下不表,下一章節會介紹其作用。ClassName的取值是子類的包名+類名,作用是通過類載入器以及雙親委派機制例項化子類,從而捨棄了初代框架中的固定的Java Bean,將框架和具體行為的類名解耦,框架只負責流程的執行而不用關心流程中涉及哪些行為類,從而解決了擴充套件性的問題。
remark的作用是對className的應用場景做一個補充,比如呼叫某微服務查詢介面,讓每個行為的意圖都儲存在case中,增加可維護性。
圖三
確定了父類之後,我們把初代框架第三層中的所有Util類都套上了一層裝飾器(Behavior的子類),主體邏輯在放在CompareIntf實現中。拿web開發類比的話,Behavior類似Controller,用來作為資料的入口,而Util類和Service類一樣,負責功能的主體邏輯。
接著我們又把行為類進行分類,把功能類似的行為類放到同一個工程作為一個Library,這樣Library與框架,Library與Library之間都完成了解耦。
解決了父類的抽象和擴充套件性問題,就差一個框架把所有的行為類整合成一條完整的自動化case了。
AutoTestFrame
圖四
在分析自動化用例執行流程時,我們發現和傳統基於BDD開發的工具不一樣的是,測試用例的執行流程give-when-then非常固定。參考圖四左邊流程圖,把用例分割成三個部分資料準備-操作步驟-資料清理,首先執行資料準備,執行成功則執行操作步驟-資料清理,失敗的話跳過操作步驟,直接執行資料清理,保證case不管成功失敗對整個環境都是冪等操作。
用例的三個部分,不管是資料準備還是操作步驟,通常都不是單獨的一個行為類,而是行為類的集合。我們把多個行為類組成一個StepSet,資料準備和資料清理都是一個StepSet,操作步驟有點特殊,會並行執行的多個StepSet。
StepSet的執行邏輯參考圖四1右邊的流程圖,序列執行行為類的compareExpectAndActual方法,返回true則執行下一個,返回false則break整個流程,case執行失敗。執行過程中行為類會和兩個上下文發生資料互動,也就是上個章節行為類的兩個Map成員。
基於以上的分析,我們在DSL中去掉了流程關鍵字(give-when-then),把流程控制硬編碼在框架中。至此第一個action兩part都解決了,框架的雛形接近完成,也該有個名字了,AutoTestFrame,簡稱ATF。
DSL選擇
這一章沒有前面那麼多去偽存真的過程,ATF還是沿用了初版框架中Json作為DSL,而沒有選擇BDD推薦的自然語言。原因有三個:第一,Json在51微服務的架構中具有天生的優勢,服務間的通訊使用Http+Json的REST規範,處於親兒子的地位;第二,KV 類語言有著接近自然語言的表現力,除了Json還有xml,yaml等,初代框架的指令碼在加上remark之後基本處於可讀的狀態;第三,Json to Java有著豐富的第三方庫,Jackson、FastJson、Gson等,省去了編寫和維護DSL解析器的精力。
既然是DSL,在易讀的同時也會有一些抽象的特性來簡化操作。ATF通過佔位符配合關鍵字開發了一些常用的特性,比如時間函式(參考圖五)、Jpath、加解密等,這些DSL會在行為類初始化之前被替換為實際值。
圖五
框架的應用
到此為止,ATF通過引入ClassName和類載入器、規範case執行流程、確定DSL解決了擴充套件性問題,通過提取上下文並賦予行為類的上下文訪問許可權解決了資料重用問題,那開篇提到的問題有沒有解決?實際在專案中使用的結果如何?
先來看下開篇提到的功能最終的指令碼,參考圖六,最終我們把時序圖拆成了三個部分來實現集測自動化,圖中的指令碼是從傳送Kafka訊息開始一直到倒數第三個服務結束。指令碼基本還原了時序圖,具備不錯的可讀性,即使換一個完全不瞭解該業務的測試工程師,也能很快領會指令碼的意圖。至於case的debug,和Jenkins的打通由於篇幅有限,這裡就不過多介紹了。
ATF在51信用卡實施了一年多,有一個Library專門維護集測常用的60多個行為類,包括各種訊息中介軟體、DB、Http等。除此之外,我們也開發了和Appium、Selenium相關的Library,還在試用階段。不完全統計,目前已經使用ATF的專案超過50個,平均集測覆蓋率超過30%,核心業務的覆蓋率超過60%。
圖六
RobotFramework
太陽底下沒有新鮮事,51信用卡經過三次迭代開發的集測自動化框架ATF,並不是在自動化領域第一個吃螃蟹的。Robot Framework is a Python-based, extensible keyword-driven test automation framework for end-to-end acceptance testing and acceptance-test-driven development (ATDD). It can be used for testing distributed, heterogeneous applications, where verification requires touching several technologies and interfaces.目前官方提供超過60個Libraries,不僅涉及整合測試領域,還包括App、Web、DeskTop Client等多個領域。雖然RobotFramework是用Python編寫,但是可以通過一個擴充套件LibraryJavalibCore作為膠水來粘合Java編寫的Library。除了DSL外,RobotFramework還有一個DesckTop Client,支援通過GUI來編輯DSL,進一步降低了自動化的准入門檻。
表面上看我們似乎是照貓畫虎造了個Java版的RF,還是簡易版的,都是用同一種思想解決同樣的領域問題。實際上,就像每種程式語言都會有獨立的HttpClient Library,在自動化領域,Library之於框架的價值等同於Library之於程式語言的價值,它們才是背後默默奉獻的螺絲釘。雖然RF有類似膠水語言的Library來解決跨語言的問題,可能並沒有JVM的內部呼叫來的可靠。所以,無論是在Library和業務貼合性上,還是在框架語言、DSL選擇上,對於51信用卡現有微服務的架構和技術棧,ATF都更適合在當前體系下扮演整合測試自動化框架的角色。
總結
隨著ATF支撐的服務數量和case數量的增加,我們遇到了很多新的問題。當單服務的case數量超過一定數量時,超過了設定的閾值(5分鐘),ATF新增了用例過濾策略、併發執行策略(圖七)來縮短執行時間。今年年初我們將ATF整合進用例管理平臺,通過web頁面也可以完成自動化用例編寫。在實現remote excutor時,我們利用URLClassLoader自定義管理Libraries,實現了行為類熱載入,目前正在試用階段,後面還會支援高可用高併發等更多特性。好的框架一定是與時俱進,通過不斷迭代解決實際的問題,ATF還很年輕,我們相信它還會有第四、第五、第N次迭代。
本文轉載自公眾號: 51NB技術, 點選檢視原文。