ApiTestEngine 演化之路(1)搭建基礎框架
在《ApiTestEngine 演化之路(0)開發未動,測試先行》一文中,我對ApiTestEngine項目正式開始前的準備工作進行了介紹,包括構建API介面服務(Mock Server)、搭建項目單元測試框架、實現持續集成構建檢查機制(Travis CI)等。
接下來,我們就開始構建ApiTestEngine項目的基礎框架,實現基本功能吧。
介面測試的核心要素
既然是從零開始,那我們不妨先想下,對於介面測試來說,最基本最核心的要素有哪些?
事實上,不管是手工進行介面測試,還是自動化測試平台執行介面測試,介面測試的核心要素都可以概括為如下三點:
- 發起介面請求(Request)
- 解析介面響應(Response)
- 檢查介面測試結果
這對於任意類型的介面測試也都是適用的。
在本系列文章中,我們關注的是API介面的測試,更具體地,是基於HTTP協議的API介面的測試。所以我們的問題就進一步簡化了,只需要關注HTTP協議層面的請求和響應即可。
好在對於絕大多數介面系統,都有明確的API介面文檔,裡面會定義好介面請求的參數(包括Headers和Body),並同時描述好介面響應的內容(包括Headers和Body)。而我們需要做的,就是根據介面文檔的描述,在HTTP請求中按照介面規範填寫請求的參數,然後讀取介面的HTTP響應內容,將介面的實際響應內容與我們的預期結果進行對比,以此判斷介面功能是否正常。這裡的預期結果,應該是包含在介面測試用例裡面的。
由此可知,實現介面測試框架的第一步是完成對HTTP請求響應處理的支持。
HTTP客戶端的最佳選擇
ApiTestEngine項目選擇Python作為編程語言,而在Python中實現HTTP請求,毫無疑問,Requests庫是最佳選擇,簡潔優雅,功能強大,可輕鬆支持API介面的多種請求方法,包括GET/POST/HEAD/PUT/DELETE等。
並且,更贊的地方在於,Requests庫針對所有的HTTP請求方法,都可以採用一套統一的介面。
requests.request(method, url, **kwargs)n
其中,kwargs中可以包含HTTP請求的所有可能需要用到的信息,例如headers、cookies、params、data、auth等。
這有什麼好處呢?
好處在於,這可以幫助我們輕鬆實現測試數據與框架代碼的分離。我們只需要遵循Requests庫的參數規範,在介面測試用例中復用Requests參數的概念即可。而對於框架的測試用例執行引擎來說,處理邏輯就異常簡單了,直接讀取測試用例中的參數,傳參給Requests發起請求即可。
如果還感覺不好理解,沒關係,直接看案例。
測試用例描述
在我們搭建的API介面服務(Mock Server)中,我們想測試「創建一個用戶,該用戶之前不存在」的場景
在上一篇文章中,我們也在unittest中對該測試場景實現了測試腳本。
def test_create_user_not_existed(self):n self.clear_users()nn url = "%s/api/users/%d" % (self.host, 1000)n data = {n "name": "user1",n "password": "123456"n }n resp = self.api_client.post(url, json=data)nn self.assertEqual(201, resp.status_code)n self.assertEqual(True, resp.json()["success"])n
在該用例中,我們實現了HTTP POST請求,api_client.post(url, json=data),然後對響應結果進行解析,並檢查resp.status_code、resp.json()["success"]是否滿足預期。
可以看出,採用代碼編寫測試用例時會用到許多編程語言的語法,對於不會編程的人來說上手難度較大。更大的問題在於,當我們編寫大量測試用例之後,因為模式基本都是固定的,所以會發現存在大量相似或重複的腳本,這給腳本的維護帶來了很大的問題。
那如何將測試用例與腳本代碼進行分離呢?
考慮到JSON格式在編程語言中處理是最方便的,分離後的測試用例可採用JSON描述如下:
{n "name": "create user which does not exist",n "request": {n "url": "http://127.0.0.1:5000/api/users/1000",n "method": "POST",n "headers": {n "content-type": "application/json"n },n "json": {n "name": "user1",n "password": "123456"n }n },n "response": {n "status_code": 201,n "headers": {n "Content-Type": "application/json"n },n "body": {n "success": true,n "msg": "user created successfully."n }n }n}n
不難看出,如上JSON結構體包含了測試用例的完整描述信息。
需要特別注意的是,這裡使用了一個討巧的方式,就是在請求的參數中充分復用了Requests的參數規範。例如,我們要POST一個JSON的結構體,那麼我們就直接將json作為request的參數名,這和前面寫腳本時用的api_client.post(url, json=data)是一致的。
測試用例執行引擎
在如上測試用例描述的基礎上,測試用例執行引擎就很簡單了,以下幾行代碼就足夠了。
def run_single_testcase(testcase):n req_kwargs = testcase[request]nn try:n url = req_kwargs.pop(url)n method = req_kwargs.pop(method)n except KeyError:n raise exception.ParamsError("Params Error")nn resp_obj = requests.request(url=url, method=method, **req_kwargs)n diff_content = utils.diff_response(resp_obj, testcase[response])n success = False if diff_content else Truen return success, diff_contentn
可以看出,不管是什麼HTTP請求方法的用例,該執行引擎都是適用的。
只需要先從測試用例中獲取到HTTP介面請求參數,testcase[request]:
{n "url": "http://127.0.0.1:5000/api/users/1000",n "method": "POST",n "headers": {n "content-type": "application/json"n },n "json": {n "name": "user1",n "password": "123456"n }n}n
然後發起HTTP請求:
requests.request(url=url, method=method, **req_kwargs)n
最後再檢查測試結果:
utils.diff_response(resp_obj, testcase[response])n
在測試用例執行引擎完成後,執行測試用例的方式也很簡單。同樣是在unittest中調用執行測試用例,就可以寫成如下形式:
def test_run_single_testcase_success(self):n testcase_file_path = os.path.join(os.getcwd(), test/data/demo.json)n testcases = utils.load_testcases(testcase_file_path)n success, _ = self.test_runner.run_single_testcase(testcases[0])n self.assertTrue(success)n
可以看出,模式還是很固定:載入用例、執行用例、判斷用例執行是否成功。如果每條測試用例都要在unittest.TestCase分別寫一個單元測試進行調用,還是會存在大量重複工作。
所以比較好的做法是,再實現一個單元測試用例生成功能;這部分先不展開,後面再進行詳細描述。
結果判斷處理邏輯
這裡再單獨講下對結果的判斷邏輯處理,也就是diff_response函數。
def diff_response(resp_obj, expected_resp_json)n diff_content = {}n resp_info = parse_response_object(resp_obj)nn # 對比 status_code,將差異存入 diff_contentn # 對比 Headers,將差異存入 diff_contentn # 對比 Body,將差異存入 diff_contentnn return diff_contentn
其中,expected_resp_json參數就是我們在測試用例中描述的response部分,作為測試用例的預期結果描述信息,是判斷實際介面響應是否正常的參考標準。
而resp_obj參數,就是實際介面響應的Response實例,詳細的定義可以參考requests.Response描述文檔。
為了更好地實現結果對比,我們也將resp_obj解析為與expected_resp_json相同的數據結構。
def parse_response_object(resp_obj):n try:n resp_body = resp_obj.json()n except ValueError:n resp_body = resp_obj.textnn return {n status_code: resp_obj.status_code,n headers: resp_obj.headers,n body: resp_bodyn }n
那麼最後再進行對比就很好實現了,只需要編寫一個通用的JSON結構體比對函數即可。
def diff_json(current_json, expected_json):n json_diff = {}nn for key, expected_value in expected_json.items():n value = current_json.get(key, None)n if str(value) != str(expected_value):n json_diff[key] = {n value: value,n expected: expected_valuen }nn return json_diffn
這裡只羅列了核心處理流程的代碼實現,其它的輔助功能,例如載入JSON/YAML測試用例等功能,請直接閱讀閱讀項目源碼。
總結
經過本文中的工作,我們已經完成了ApiTestEngine基礎框架的搭建,並實現了兩項最基本的功能:
- 支持API介面的多種請求方法,包括 GET/POST/HEAD/PUT/DELETE 等
- 測試用例與代碼分離,測試用例維護方式簡潔優雅,支持YAML/JSON
然而,在實際項目中的介面通常比較複雜,例如包含簽名校驗等機制,這使得我們在配置介面測試用例時還是會比較繁瑣。
在下一篇文章中,我們將著手解決這個問題,通過對框架增加模板配置功能,實現介面業務參數和技術細節的分離。
閱讀更多
《介面自動化測試的最佳工程實踐(ApiTestEngine)》
《ApiTestEngine 演化之路(0)開發未動,測試先行》ApiTestEngine GitHub源碼
推薦閱讀: