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

背景

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

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

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

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

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

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

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

不妨舉個最常見的例子。

某個介面的請求參數非常多,並且介面請求要求有MD5簽名校驗;簽名的方式為在Headers中包含一個sign參數,該參數值通過對URL、Method、Body的拼接字元串進行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 objectrequest_meta["method"] = methodrequest_meta["start_time"] = time.time()response = self._send_request_safe_mode(method, url, **kwargs)# record the consumed timerequest_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庫的強大功能,同時也能將請求的響應時間、響應體大小等原始性能數據進行保存,實現可謂十分優雅。

受到該處啟發,要保存介面的詳細請求響應數據也可採用同樣的方式。例如,要保存Response的Headers、Body只需要增加如下兩行代碼:

request_meta["response_headers"] = response.headersrequest_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_session是requests.Session的實例,用於進行介面測試;當需要進行性能測試時,只需要傳入locust的HttpSession實例即可。

具有可擴展性,便於擴展實現Web平台化

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

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

寫在後面

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

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

GitHub項目地址:debugtalk/ApiTestEngine

參考

《打造心目中理想的自動化測試框架(AppiumBooster)》

《告別robotframework》

《Advanced Guide For PyRestTest》

歡迎關注個人原創微信公眾號:DebugTalk


推薦閱讀:

Python 開發中有哪些高級技巧?
如何在mac版本的python里安裝pip?
Github上同學總結的機器學習和deeplearning方面的很全的資料
python有什麼是可以用matlab代替實現,甚至更有優勢的嗎?
python中的綁定方法和未綁定方法是什麼?

TAG:API | 自动化测试 | Python |