介面自動化測試的最佳工程實踐(ApiTestEngine)

debugtalk發表於2019-03-04

背景

當前市面上存在的介面測試工具已經非常多,常見的如PostmanJMeterRobotFramework等,相信大多數測試人員都有使用過,至少從接觸到的大多數簡歷的描述上看是這樣的。除了這些成熟的工具,也有很多有一定技術能力的測試(開發)人員自行開發了一些介面測試框架,質量也是參差不齊。

但是,當我打算在專案組中推行介面自動化測試時,蒐羅了一圈,也沒有找到一款特別滿意的工具或框架,總是與理想中的構想存在一定的差距。

那麼理想中的介面自動化測試框架應該是怎樣的呢?

測試工具(框架)脫離業務使用場景都是耍流氓!所以我們不妨先來看下日常工作中的一些常見場景。

  • 測試或開發人員在定位問題的時候,想呼叫某個介面檢視其是否響應正常;
  • 測試人員在手工測試某個功能點的時候,需要一個訂單號,而這個訂單號可以通過順序呼叫多個介面實現下單流程;
  • 測試人員在開始版本功能測試之前,可以先檢測下系統的所有介面是否工作正常,確保介面正常後再開始手工測試;
  • 開發人員在提交程式碼前需要檢測下新程式碼是否對系統的已有介面產生影響;
  • 專案組需要每天定時檢測下測試環境所有介面的工作情況,確保當天的提交程式碼沒有對主幹分支的程式碼造成破壞;
  • 專案組需要定時(30分鐘)檢測下生產環境所有介面的工作情況,以便及時發現生產環境服務不可用的情況;
  • 專案組需要不定期對核心業務場景進行效能測試,期望能減少人力投入,直接複用介面測試中的工作成果。

可以看到,以上羅列的場景大家應該都很熟悉,這都是我們在日常工作中經常需要去做的事情。但是在沒有一款合適工具的情況下,效率往往十分低下,或者就是某些重要工作壓根就沒有開展,例如介面迴歸測試、線上介面監控等。

先說下最簡單的手工呼叫介面測試。可能有人會說,Postman就可以滿足需求啊。的確,Postman作為一款通用的介面測試工具,它可以構造介面請求,檢視介面響應,從這個層面上來說,它是滿足了介面測試的功能需求。但是在具體的專案中,使用Postman並不是那麼高效。

不妨舉個最常見的例子。

某個介面的請求引數非常多,並且介面請求要求有MD5簽名校驗;簽名的方式為在Headers中包含一個sign引數,該引數值通過對URLMethodBody的拼接字串進行MD5計算後得到。

回想下我們要對這個介面進行測試時是怎麼做的。首先,我們需要先參照介面文件的描述,手工填寫完所有介面引數;然後,按照簽名校驗方式,對所有引數值進行拼接得到一個字串,在另一個MD5計算工具計算得到其MD5值,將簽名值填入sign引數;最後,才是發起介面請求,檢視介面響應,並人工檢測響應是否正常。最坑爹的是,我們每次需要呼叫這個介面的時候,以上工作就得重新來一遍。這樣的實際結果是,面對引數較多或者需要簽名驗證的介面時,測試人員可能會選擇忽略不進行介面測試。

除了單個介面的呼叫,很多時候我們也需要組合多個介面進行呼叫。例如測試人員在測試物流系統時,經常需要一個特定組合條件下生成的訂單號。而由於訂單號關聯的業務較多,很難直接在資料庫中生成,因此當前業務測試人員普遍採取的做法,就是每次需要訂單號時模擬下單流程,順序呼叫多個相應的介面來生成需要的訂單號。可以想象,在手工呼叫單個介面都如此麻煩的情況下,每次都要手工呼叫多個介面會有多麼的費時費力。

再說下介面自動化呼叫測試。這一塊兒大多介面測試框架都支援,普遍的做法就是通過程式碼編寫介面測試用例,或者採用資料驅動的方式,然後在支援命令列(CLI)呼叫的情況下,就可以結合Jenkins或者crontab實現持續整合,或者定時介面監控的功能。

思路是沒有問題的,問題在於實際專案中的推動落實情況。要說自動化測試用例最靠譜的維護方式,還是直接通過程式碼編寫測試用例,可靠且不失靈活性,這也是很多經歷過慘痛教訓的老手的感悟,甚至網路上還出現了一些反測試框架的言論。但問題在於專案中的測試人員並不是都會寫程式碼,也不是對其強制要求就能馬上學會的。這種情況下,要想在具體專案中推動介面自動化測試就很難,就算我可以幫忙寫一部分,但是很多時候介面測試用例也是要結合業務邏輯場景的,我也的確是沒法在這方面投入太多時間,畢竟對接的專案實在太多。所以也是基於這類原因,很多測試框架提倡採用資料驅動的方式,將業務測試用例和執行程式碼分離。不過由於很多時候業務場景比較複雜,大多數框架測試用例模板引擎的表達能力不足,很難採用簡潔的方式對測試場景進行描述,從而也沒法很好地得到推廣使用。

可以列舉的問題還有很多,這些也的確都是在網際網路企業的日常測試工作中真實存在的痛點。

基於以上背景,我產生了開發ApiTestEngine的想法。

對於ApiTestEngine的定位,與其說它是一個工具或框架,它更多的應該是一套介面自動化測試的最佳工程實踐,而簡潔優雅實用應該是它最核心的特點。

當然,每位工程師對最佳工程實踐的理念或多或少都會存在一些差異,也希望大家能多多交流,在思維的碰撞中共同進步。

核心特性

ApiTestEngine的核心特性概述如下:

  • 支援API介面的多種請求方法,包括 GET/POST/HEAD/PUT/DELETE 等
  • 測試用例與程式碼分離,測試用例維護方式簡潔優雅,支援YAML
  • 測試用例描述方式具有表現力,可採用簡潔的方式描述輸入引數和預期輸出結果
  • 介面測試用例具有可複用性,便於建立複雜測試場景
  • 測試執行方式簡單靈活,支援單介面呼叫測試、批量介面呼叫測試、定時任務執行測試
  • 測試結果統計報告簡潔清晰,附帶詳盡日誌記錄,包括介面請求耗時、請求響應資料等
  • 身兼多職,同時實現介面管理、介面自動化測試、介面效能測試(結合Locust)
  • 具有可擴充套件性,便於擴充套件實現Web平臺化

特性拆解介紹

支援API介面的多種請求方法,包括 GET/POST/HEAD/PUT/DELETE 等

個人偏好,程式語言選擇Python。而採用Python實現HTTP請求,最好的方式就是採用Requests庫了,簡潔優雅,功能強大。

測試用例與程式碼分離,測試用例維護方式簡潔優雅,支援YAML

要實現測試用例與程式碼的分離,最好的做法就是做一個測試用例載入引擎和一個測試用例執行引擎,這也是之前在做AppiumBooster框架的時候總結出來的最優雅的實現方式。當然,這裡需要事先對測試用例制定一個標準的資料結構規範,作為測試用例載入引擎和測試用例執行引擎的橋樑。

需要說明的是,測試用例資料結構必須包含介面測試用例完備的資訊要素,包括介面請求的資訊內容(URL、Headers、Method等引數),以及預期的介面請求響應結果(StatusCode、ResponseHeaders、ResponseContent)。

這樣做的好處在於,不管測試用例採用什麼形式進行描述(YAML、JSON、CSV、Excel、XML等),也不管測試用例是否採用了業務分層的組織思想,只要在測試用例載入引擎中實現對應的轉換器,都可以將業務測試用例轉換為標準的測試用例資料結構。而對於測試用例執行引擎而言,它無需關注測試用例的具體描述形式,只需要從標準的測試用例資料結構中獲取到測試用例資訊要素,包括介面請求資訊和預期介面響應資訊,然後構造併發起HTTP請求,再將HTTP請求的響應結果與預期結果進行對比判斷即可。

至於為什麼明確說明支援YAML,這是因為個人認為這是最佳的測試用例描述方式,表達簡潔不累贅,同時也能包含非常豐富的資訊。當然,這只是個人喜好,如果喜歡採用別的方式,只需要擴充套件實現對應的轉換器即可。

測試用例描述方式具有表現力,可採用簡潔的方式描述輸入引數和預期輸出結果

測試用例與框架程式碼分離以後,對業務邏輯測試場景的描述重任就落在測試用例上了。比如我們選擇採用YAML來描述測試用例,那麼我們就應該能在YAML中描述各種複雜的業務場景。

那麼怎麼理解這個“表現力”呢?

簡單的引數值傳參應該都容易理解,我們舉幾個相對複雜但又比較常見的例子。

  • 介面請求引數中要包含當前的時間戳;
  • 介面請求引數中要包含一個16位的隨機字串;
  • 介面請求引數中包含簽名校驗,需要對多個請求引數進行拼接後取md5值;
  • 介面響應頭(Headers)中要包含一個X-ATE-V頭域,並且需要判斷該值是否大於100;
  • 介面響應結果中包含一個字串,需要校驗字串中是否包含10位長度的訂單號;
  • 介面響應結果為一個多層巢狀的json結構體,需要判斷某一層的某一個元素值是否為True。

可以看出,以上幾個例子都是沒法直接在測試用例裡面描述引數值的。如果是採用Python指令碼來編寫測試用例還好解決,只需要通過Python函式實現即可。但是現在測試用例和框架程式碼分離了,我們沒法在YAML裡面執行Python函式,這該怎麼辦呢?

答案就是,定義函式轉義符,實現自定義模板。

這種做法其實也不難理解,也算是模板語言通用的方式。例如,我們將${}定義為轉義符,那麼在{}內的內容就不再當做是普通的字串,而應該轉義為變數值,或者執行函式得到實際結果。當然,這個需要我們在測試用例執行引擎進行適配實現,最簡單方式就是提取出${}中的字串,通過eval計算得到表示式的值。如果要實現更復雜的功能,我們也可以將介面測試中常用的一些功能封裝為一套關鍵字,然後在編寫測試用例的時候使用這些關鍵字。

介面測試用例具有可複用性,便於建立複雜測試場景

很多情況下,系統的介面都是有業務邏輯關聯的。例如,要請求呼叫登入介面,需要先請求獲取驗證碼的介面,然後在登入請求中帶上獲取到的驗證碼;而要請求資料查詢的介面,又要在請求引數中包含登入介面返回的session值。這個時候,我們如果針對每一個要測的業務邏輯,都單獨描述要請求的介面,那麼就會造成大量的重複描述,測試用例的維護也十分臃腫。

比較好的做法是,將每一個介面呼叫單獨封裝為一條測試用例,然後在描述業務測試場景時,選擇對應的介面,按照順序拼接為業務場景測試用例,就像搭積木一般。如果你之前讀過AppiumBooster的介紹,應該還會聯想到,我們可以將常用的功能組成模組用例集,然後就可以在更高的層面對模組用例集進行組裝,實現更復雜的測試場景。

不過,這裡有一個非常關鍵的問題需要解決,就是如何在介面測試用例之前傳參的問題。其實實現起來也不復雜,我們可以在介面請求響應結果中指定一個變數名,然後將介面返回關鍵值提取出來後賦值給那個變數;然後在其它介面請求引數中,傳入這個${變數名}即可。

測試執行方式簡單靈活,支援單介面呼叫測試、批量介面呼叫測試、定時任務執行測試

通過背景中的例子可以看出,需要使用介面測試工具的場景很多,除了定時地對所有介面進行自動化測試檢測外,很多時候在手工測試的時候也需要採用介面測試工具進行輔助,也就是半手工+半自動化的模式。

而業務測試人員在使用測試工具的時候,遇到的最大問題在於除了需要關注業務功能本身,還需要花費很多時間去處理技術實現細節上的東西,例如簽名校驗這類情況,而且往往後者在重複操作中佔用的時間更多。

這個問題的確是沒法避免的,畢竟不同系統的介面千差萬別,不可能存在一款工具可以自動處理所有情況。但是我們可以嘗試將介面的技術細節實現和業務引數進行拆分,讓業務測試人員只需要關注業務引數部分。

具體地,我們可以針對每一個介面配置一個模板,將其中與業務功能無關的引數以及技術細節封裝起來,例如簽名校驗、時間戳、隨機值等,而與業務功能相關的引數配置為可傳參的模式。

這樣做的好處在於,與業務功能無關的引數以及技術細節我們只需要封裝配置一次,而且這個工作可以由開發人員或者測試開發人員來實現,減輕業務測試人員的壓力;介面模板配置好後,測試人員只需要關注與業務相關的引數即可,結合業務測試用例,就可以在介面模板的基礎上很方便地配置生成多個介面測試用例。

測試結果統計報告簡潔清晰,附帶詳盡日誌記錄,包括介面請求耗時、請求響應資料等

測試結果統計報告,應該遵循簡潔而不簡單的原則。“簡潔”,是因為大多數時候我們只需要在最短的時間內判斷所有介面是否執行正常即可。而“不簡單”,是因為當存在執行失敗的測試用例時,我們期望能獲得介面測試時儘可能詳細的資料,包括測試時間、請求引數、響應內容、介面響應耗時等。

之前在讀locust原始碼時,其對HTTP客戶端的封裝方式給我留下了深刻的印象。它採用的做法是,繼承requests.Session類,在子類HttpSession中重寫覆蓋了request方法,然後在request方法中對requests.Session.request進行了一層封裝。

request_meta = {}

# set up pre_request hook for attaching meta data to the request object
request_meta["method"] = method
request_meta["start_time"] = time.time()

response = self._send_request_safe_mode(method, url, **kwargs)

# record the consumed time
request_meta["response_time"] = int((time.time() - request_meta["start_time"]) * 1000)

request_meta["content_size"] = int(response.headers.get("content-length") or 0)複製程式碼

HttpLocust的每一個虛擬使用者(client)都是一個HttpSession例項,這樣每次在執行HTTP請求的時候,既可充分利用Requests庫的強大功能,同時也能將請求的響應時間、響應體大小等原始效能資料進行儲存,實現可謂十分優雅。

受到該處啟發,要儲存介面的詳細請求響應資料也可採用同樣的方式。例如,要儲存ResponseHeadersBody只需要增加如下兩行程式碼:

request_meta["response_headers"] = response.headers
request_meta["response_content"] = response.content複製程式碼

身兼多職,同時實現介面管理、介面自動化測試、介面效能測試(結合Locust)

其實像介面效能測試這樣的需求,不應該算到介面自動化測試框架的職責範圍之內。但是在實際專案中需求就是這樣,又要做介面自動化測試,又要做介面效能測試,而且還不想同時維護兩套程式碼。

多虧有了locust效能測試框架,介面自動化和效能測試指令碼還真能合二為一。

前面也講了,HttpLocust的每一個虛擬使用者(client)都是一個HttpSession例項,而HttpSession又繼承自requests.Session類,所以HttpLocust的每一個虛擬使用者(client)也是requests.Session類的例項。

同樣的,我們在用Requests庫做介面測試時,請求客戶端其實也是requests.Session類的例項,只是我們通常用的是requests的簡化用法。

以下兩種用法是等價的。

resp = requests.get(`http://debugtalk.com`)

# 等價於
client = requests.Session()
resp = client.get(`http://debugtalk.com`)複製程式碼

有了這一層關係以後,要在介面自動化測試和效能測試之間切換就很容易了。在介面測試框架內,可以通過如下方式初始化HTTP客戶端。

def __init__(self, origin, kwargs, http_client_session=None):
   self.http_client_session = http_client_session or requests.Session()複製程式碼

預設情況下,http_client_sessionrequests.Session的例項,用於進行介面測試;當需要進行效能測試時,只需要傳入locustHttpSession例項即可。

具有可擴充套件性,便於擴充套件實現Web平臺化

當要將測試平臺推廣至更廣闊的使用者群體(例如產品經理、運營人員)時,對框架實現Web化就在所難免了。在Web平臺上檢視介面測試用例執行情況、對介面模組進行配置、對介面測試用例進行管理,的確會便捷很多。

不過對於介面測試框架來說,Web平臺只能算作錦上添花的功能。我們在初期可以優先實現命令列(CLI)呼叫方式,規範好資料儲存結構,後期再結合Web框架(如Flask)增加實現Web平臺功能。

寫在後面

以上便是我對ApiTestEngine特性的詳細介紹,也算是我個人對介面自動化測試最佳工程實踐的理念闡述。

當前,ApiTestEngine還處於開發過程中,程式碼也開源託管在GitHub上,歡迎Star關注。

GitHub專案地址:github.com/debugtalk/A…

參考

相關文章