51 信用卡微服務集成測試自動化探索

1 簡介

51 信用卡管家自 2015 年開始實施微服務架構,是業界較早嘗試微服務架構的技術團隊,整個團隊有幸見證了微服務從最初的個服務試點到全面鋪開的過程。架構的演變也催生了自動化測試框架和策略的演變,測試團隊通過持續地探索和總結,在集成測試自動化框架建設和策略選擇上積累了一些經驗,拋磚引玉和大家一起分享。

 

2 微服務架構下集成測試自動化的困境

2.1 微服務架構給測試帶來的改變

先看下《微服務設計》1.3 章節對 SOA 的定義:

 

SOA(Service-Oriented Architecture,面向服務的架構)是一種設計方法,其中包含多個服務,而服務之間通過配合最終會提供一系列的功能。一個服務通常以獨立的形式存在於操作系統進程中。服務之間通過網絡調用,而非採用進程內調用的方式進行通信。

 

微服務架構是 SOA 的一種特定方法,基於這種架構在開發層面帶來的好處在《微服務設計》一書中描述得很清楚了。從測試角度來看,通信方式由進程內調用變成網絡調用,最明顯的改變有兩個:

 

第一,數據流轉更加清晰。微服務架構下的時序圖非常清晰,服務之間分工明確,代替了傳統服務數據只在服務內部模塊間流轉的方式。

 

第二,數據入口多元化,可測試性增強。服務的拆分帶來的另一個好處就是測試粒度變小變細,每個微服務都具備獨立的網絡調用方式,不管是提供 Http 接口還是消費消息隊列,都給集成測試可測試性和覆蓋率帶來了便利和提升。

 

2.2 集成測試自動化的困境

軟件工程沒有銀彈,對於微服務也是一樣。在《微服務設計》1.1.1 章節作者 Sam Newman 對微服務架構的擔憂:

 

當考慮(微服務)多小才足夠小的時候,我會考慮這些因素:服務越小,微服務架構的優點和缺點也就越明顯。使用的服務越小,獨立性帶來的好處就越多。但是管理大量服務也會越複雜,本書的剩餘部分會詳細討論這一複雜性。如果你能夠更好地處理這一複雜性,那麼就可以盡情地使用微服務了。

 

一方面服務拆分給測試帶來很多便利,另一方面服務數量的增加也帶來了測試複雜度的指數增長,好像陷入了一種囚徒困境。

 

以圖 2.2.1 的功能爲例,該功能一共涉及了 7 個微服務,2 個消息中間件,還有沒在時序圖上體現的兩種 DB(mysql/cassandra)和緩存中間件(redis)。由於服務和中間件之間的調用也是網絡調用,所以在測試過程可以把中間件和 DB 當成一個微服務節點來分析。面對這樣一個涉及 12 個微服務的功能(不考慮集測自動化策略,比如拆分成幾個小功能分塊實現自動化降低複雜度),我們需要一個什麼樣的框架來支撐,才能讓測試工程師寫出的集測自動化 case 具備易用性、可維護性這些美好的特性。

 

 

2.2.1 功能 A 服務調用時序圖

 

2.3 拆解困境

當時 51 信用卡的集測框架大概使用了一年多,中間迭代過兩個大的版本,已經從最原始的純 Java 框架過渡到基於關鍵字驅動和數據驅動開發的框架,原理參考圖 2.3.1。

 

 

圖 2.3.1

 

初代框架第一層是測試腳本(參考下圖的代碼)和 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"    }  }}
 
 
 
複製代碼
 

 

初代框架在面臨圖 2.2.1 複雜的功能時,最重要的一個問題就是擴展性。腳本的每個 modul 都代表了一次網絡調用,可讀性很好。但是測試流程被固化在每個 Java Bean 之中,每一個新的功能都有可能要定製一個新的 Java Bean(調整 action 調用順序)和 DataProvider(圖 2.3.1 藍色部分)。而當 Util 類變更時,同樣需要變更 Java Bean 和 DataProvider,這個變更的工作量堪稱地獄級。

 

第二個問題是數據的重用。圖 2.3.2 中的 userid 這個字段在腳本中出現 3 次,在某些複雜場景下可能就不是一個字段而是整個數據結構,降低了 case 的可維護性。數據庫連接和中間件連接(腳本中的 redis client)每個 case 都會先初始化再銷燬,這個對於執行效率也是一種影響。

 

基礎框架有了,問題也找到了,下面我們要做的就是改造基礎框架來滿足新的自動化需求。

 

3 造一個“輪子”逃出困境

3.1 BDD&DSL

In software engineering, behavior-driven development (BDD) is a software development process that emerged from test-driven development(TDD).[1][2][3][vague] Behavior-driven development combines the general techniques and principles of TDD with ideas from domain-driven design and object-oriented analysis and design to provide software development and management teams with shared tools and a shared process to collaborate on software development.[1][4]

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.[5]

 

在尋找擴展性解決方案的過程中,瞭解到兄弟部門正在嘗試用 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 是否正確。

 

3.2 行爲父類 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 中,增加可維護性。

 

 

圖 3.2.1 框架類圖

 

確定了父類之後,我們把初代框架第三層中的所有 Util 類都套上了一層裝飾器(Behavior 的子類),主體邏輯在放在 CompareIntf 實現中。拿 web 開發類比的話,Behavior 類似 Controller,用來作爲數據的入口,而 Util 類和 Service 類一樣,負責功能的主體邏輯。

 

接着我們又把行爲類進行分類,把功能類似的行爲類放到同一個工程作爲一個 Library,這樣 Library 與框架,Library 與 Library 之間都完成了解耦。

 

解決了父類的抽象和擴展性問題,就差一個框架把所有的行爲類整合成一條完整的自動化 case 了。

 

3.3 AutoTestFrame

 

圖 3.3.1 自動化 case 執行流程圖

 

在分析自動化用例執行流程時,我們發現和傳統基於 BDD 開發的工具不一樣的是,測試用例的執行流程 give-when-then 非常固定。參考圖 3.3.1 左邊流程圖,把用例分割成三個部分數據準備-操作步驟-數據清理,首先執行數據準備,執行成功則執行操作步驟-數據清理,失敗的話跳過操作步驟,直接執行數據清理,保證 case 不管成功失敗對整個環境都是冪等操作。

 

用例的三個部分,不管是數據準備還是操作步驟,通常都不是單獨的一個行爲類,而是行爲類的集合。我們把多個行爲類組成一個 StepSet,數據準備和數據清理都是一個 StepSet,操作步驟有點特殊,會並行執行的多個 StepSet。

 

StepSet 的執行邏輯參考圖 3.3.1 右邊的流程圖,串行執行行爲類的 compareExpectAndActual 方法,返回 true 則執行下一個,返回 false 則 break 整個流程,case 執行失敗。執行過程中行爲類會和兩個上下文發生數據交互,也就是上個章節行爲類的兩個 Map 成員。

 

基於以上的分析,我們在 DSL 中去掉了流程關鍵字(give-when-then),把流程控制硬編碼在框架中。至此第一個 action 兩 part 都解決了,框架的雛形接近完成,也該有個名字了,AutoTestFrame,簡稱 ATF。

 

3.4 DSL 選擇

這一章沒有前面那麼多去僞存真的過程,ATF 還是沿用了初版框架中 Json 作爲 DSL,而沒有選擇 BDD 推薦的自然語言。原因有三個:第一,Json 在 51 微服務的架構中具有天生的優勢,服務間的通信使用 Http+Json 的 REST 規範,處於親兒子的地位;第二,KV 類語言有着接近自然語言的表現力,除了 Json 還有 xml,yaml 等,初代框架的腳本在加上 remark 之後基本處於可讀的狀態;第三,Json to Java 有着豐富的第三方庫,Jackson、FastJson、Gson 等,省去了編寫和維護 DSL 解析器的精力。

 

既然是 DSL,在易讀的同時也會有一些抽象的特性來簡化操作。ATF 通過佔位符配合關鍵字開發了一些常用的特性,比如時間函數(參考圖 3.4.1)、Jpath、加解密等,這些 DSL 會在行爲類初始化之前被替換爲實際值。

 

 

圖 3.4.1 時間函數 DSL

 

3.5 框架的應用

到此爲止,ATF 通過引入 ClassName 和類加載器、規範 case 執行流程、確定 DSL 解決了擴展性問題,通過提取上下文並賦予行爲類的上下文訪問權限解決了數據重用問題,那開篇提到的問題有沒有解決?實際在項目中使用的結果如何?

 

先來看下開篇提到的功能最終的腳本,參考圖 3.5.1,最終我們把時序圖拆成了三個部分來實現集測自動化,圖中的腳本是從發送 kafka 消息開始一直到倒數第三個服務結束。腳本基本還原了時序圖,具備不錯的可讀性,即使換一個完全不瞭解該業務的測試工程師,也能很快領會腳本的意圖。至於 case 的 debug,和 jenkins 的打通由於篇幅有限,這裏就不過多介紹了。

 

ATF 在 51 實施了一年多,有一個 Library 專門維護集測常用的 60 多個行爲類,包括各種消息中間件、DB、Http 等。除此之外,我們也開發了和 Appium、Selenium 相關的 Library,還在試用階段。不完全統計,目前已經使用 ATF 的項目超過 50 個,平均集測覆蓋率超過 30%,核心業務的覆蓋率超過 60%。

 

 

圖 3.5.1 ATF case 腳本

 

3.6 RobotFramework

太陽底下沒有新鮮事,我司經過三次迭代開發的集測自動化框架 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.

 https://www.infoq.cn/article/1NFdtsXtxftQt1*EMbQO?utm_source=related_read_bottom&utm_medium=article

RobotFramework 的 idea 在 2005 年出自 Pekka Klärck,同年在諾基亞開發出第一個版本,2008 年開源了 release2.0 版本,最近一次發版 V3.02 是在去年 7 月。目前官方提供超過 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 都更適合在當前體系下扮演集成測試自動化框架的角色。

 

4 總結

 

圖 4.1.1 併發執行策略

 

隨着 ATF 支撐的服務數量和 case 數量的增加,我們遇到了很多新的問題。當單服務的 case 數量超過一定數量時,超過了設定的閾值(5 分鐘),ATF 新增了用例過濾策略、併發執行策略(圖 4.1.1)來縮短執行時間。今年年初我們將 ATF 整合進用例管理平臺,通過 web 頁面也可以完成自動化用例編寫。在實現 remote excutor 時,我們利用 URLClassLoader 自定義管理 Libraries,實現了行爲類熱加載,目前正在試用階段,後面還會支持高可用高併發等更多特性。好的框架一定是與時俱進,通過不斷迭代解決實際的問題,ATF 還很年輕,我們相信它還會有第四、第五、第 N 次迭代。

 

引用:

 

  1. 《微服務設計》Sam Newman 著 崔力強 張駿譯

  2. BDD https://en.wikipedia.org/wiki/Behavior-driven_development

  3. 《從數據驅動到各種驅動》https://zhuanlan.zhihu.com/p/30588403

  4. RobotFrameword wiki https://en.wikipedia.org/wiki/Robot_Framework#cite_note-2

  5. JavaLibCore https://github.com/robotframework/JavalibCore/wiki/Getting-Started

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章