本文將通過一個簡單的示例來展示 HttpRunner 的核心功能使用方法。
案例介紹¶
該案例作爲被測服務,主要有兩類接口:
- 權限校驗,獲取 token
- 支持 CRUD 操作的 RESTful APIs,所有接口的請求頭域中都必須包含有效的 token
案例的實現形式爲 flask 應用服務(api_server.py),啓動方式如下:
$ export FLASK_APP=docs/data/api_server.py $ export FLASK_ENV=development $ flask run * Serving Flask app "docs/data/api_server.py" (lazy loading) * Environment: development * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 989-476-348
服務啓動成功後,我們就可以開始對其進行測試了。
測試準備¶
抓包分析¶
在開始測試之前,我們需要先了解接口的請求和響應細節,而最佳的方式就是採用 Charles Proxy
或者 Fiddler
這類網絡抓包工具進行抓包分析。
例如,在本案例中,我們先進行權限校驗,然後成功創建一個用戶,對應的網絡抓包內容如下圖所示:
通過抓包,我們可以看到具體的接口信息,包括請求的URL、Method、headers、參數和響應內容等內容,基於這些信息,我們就可以開始編寫測試用例了。
生成測試用例¶
爲了簡化測試用例的編寫工作,HttpRunner 實現了測試用例生成的功能。
首先,需要將抓取得到的數據包導出爲 HAR 格式的文件,假設導出的文件名稱爲 demo-quickstart.har。
導出方法:
然後,在命令行終端中運行如下命令,即可將 demo-quickstart.har 轉換爲 HttpRunner 的測試用例文件。
$ har2case docs/data/demo-quickstart.har -2y INFO:root:Start to generate testcase. INFO:root:dump testcase to YAML format. INFO:root:Generate YAML testcase successfully: docs/data/demo-quickstart.yml
使用 har2case
轉換腳本時默認轉換爲 JSON 格式,加上 -2y
參數後轉換爲 YAML 格式。兩種格式完全等價,YAML 格式更簡潔,JSON 格式支持的工具更豐富,大家可根據個人喜好進行選擇。關於 har2case 的詳細使用說明,請查看《錄製生成測試用例》。
經過轉換,在源 demo-quickstart.har 文件的同級目錄下生成了相同文件名稱的 YAML 格式測試用例文件 demo-quickstart.yml,其內容如下:
- config: name: testcase description variables: {} - test: name: /api/get-token request: headers: Content-Type: application/json User-Agent: python-requests/2.18.4 app_version: 2.8.6 device_sn: FwgRiO7CNA50DSU os_platform: ios json: sign: 9c0c7e51c91ae963c833a4ccbab8d683c4a90c98 method: POST url: http://127.0.0.1:5000/api/get-token validate: - eq: [status_code, 200] - eq: [headers.Content-Type, application/json] - eq: [content.success, true] - eq: [content.token, baNLX1zhFYP11Seb] - test: name: /api/users/1000 request: headers: Content-Type: application/json User-Agent: python-requests/2.18.4 device_sn: FwgRiO7CNA50DSU token: baNLX1zhFYP11Seb json: name: user1 password: '123456' method: POST url: http://127.0.0.1:5000/api/users/1000 validate: - eq: [status_code, 201] - eq: [headers.Content-Type, application/json] - eq: [content.success, true] - eq: [content.msg, user created successfully.]
現在我們只需要知道如下幾點:
- 每個 YAML/JSON 文件對應一個測試用例(testcase)
- 每個測試用例爲一個
list of dict
結構,其中可能包含全局配置項(config)和若干個測試步驟(test) config
爲全局配置項,作用域爲整個測試用例test
對應單個測試步驟,作用域僅限於本身
如上便是 HttpRunner 測試用例的基本結構。
關於測試用例的更多內容,請查看《測試用例結構描述》。
首次運行測試用例¶
測試用例就緒後,我們可以開始調試運行了。
爲了演示測試用例文件的迭代優化過程,我們先將 demo-quickstart.json 重命名爲 demo-quickstart-0.json(對應的 YAML 格式:demo-quickstart-0.yml)。
運行測試用例的命令爲hrun
,後面直接指定測試用例文件的路徑即可。
$ hrun docs/data/demo-quickstart-0.yml INFO Start to run testcase: testcase description /api/get-token INFO POST http://127.0.0.1:5000/api/get-token INFO status_code: 200, response_time(ms): 9.26 ms, response_length: 46 bytes ERROR validate: content.token equals baNLX1zhFYP11Seb(str) ==> fail tXGuSQgOCVXcltkz(str) equals baNLX1zhFYP11Seb(str) ERROR ******************************** DETAILED REQUEST & RESPONSE ******************************** ====== request details ====== url: http://127.0.0.1:5000/api/get-token method: POST headers: {'Content-Type': 'application/json', 'User-Agent': 'python-requests/2.18.4', 'app_version': '2.8.6', 'device_sn': 'FwgRiO7CNA50DSU', 'os_platform': 'ios'} json: {'sign': '9c0c7e51c91ae963c833a4ccbab8d683c4a90c98'} verify: True ====== response details ====== status_code: 200 headers: {'Content-Type': 'application/json', 'Content-Length': '46', 'Server': 'Werkzeug/0.14.1 Python/3.7.0', 'Date': 'Sat, 26 Jan 2019 14:43:55 GMT'} body: '{"success": true, "token": "tXGuSQgOCVXcltkz"}' F /api/users/1000 INFO POST http://127.0.0.1:5000/api/users/1000 ERROR 403 Client Error: FORBIDDEN for url: http://127.0.0.1:5000/api/users/1000 ERROR validate: status_code equals 201(int) ==> fail 403(int) equals 201(int) ERROR validate: content.success equals True(bool) ==> fail False(bool) equals True(bool) ERROR validate: content.msg equals user created successfully.(str) ==> fail Authorization failed!(str) equals user created successfully.(str) ERROR ******************************** DETAILED REQUEST & RESPONSE ******************************** ====== request details ====== url: http://127.0.0.1:5000/api/users/1000 method: POST headers: {'Content-Type': 'application/json', 'User-Agent': 'python-requests/2.18.4', 'device_sn': 'FwgRiO7CNA50DSU', 'token': 'baNLX1zhFYP11Seb'} json: {'name': 'user1', 'password': '123456'} verify: True ====== response details ====== status_code: 403 headers: {'Content-Type': 'application/json', 'Content-Length': '50', 'Server': 'Werkzeug/0.14.1 Python/3.7.0', 'Date': 'Sat, 26 Jan 2019 14:43:55 GMT'} body: '{"success": false, "msg": "Authorization failed!"}' F ====================================================================== FAIL: test_0000_000 (httprunner.api.TestSequense) /api/get-token ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 54, in test test_runner.run_test(test_dict) httprunner.exceptions.ValidationFailure: validate: content.token equals baNLX1zhFYP11Seb(str) ==> fail tXGuSQgOCVXcltkz(str) equals baNLX1zhFYP11Seb(str) During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 56, in test self.fail(str(ex)) AssertionError: validate: content.token equals baNLX1zhFYP11Seb(str) ==> fail tXGuSQgOCVXcltkz(str) equals baNLX1zhFYP11Seb(str) ====================================================================== FAIL: test_0001_000 (httprunner.api.TestSequense) /api/users/1000 ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 54, in test test_runner.run_test(test_dict) httprunner.exceptions.ValidationFailure: validate: status_code equals 201(int) ==> fail 403(int) equals 201(int) validate: content.success equals True(bool) ==> fail False(bool) equals True(bool) validate: content.msg equals user created successfully.(str) ==> fail Authorization failed!(str) equals user created successfully.(str) During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 56, in test self.fail(str(ex)) AssertionError: validate: status_code equals 201(int) ==> fail 403(int) equals 201(int) validate: content.success equals True(bool) ==> fail False(bool) equals True(bool) validate: content.msg equals user created successfully.(str) ==> fail Authorization failed!(str) equals user created successfully.(str) ---------------------------------------------------------------------- Ran 2 tests in 0.026s FAILED (failures=2) INFO Start to render Html report ... INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548513835.html
非常不幸,兩個接口的測試用例均運行失敗了。
優化測試用例¶
從兩個測試步驟的報錯信息和堆棧信息(Traceback)可以看出,第一個步驟失敗的原因是獲取的 token 與預期值不一致,第二個步驟失敗的原因是請求權限校驗失敗(403)。
接下來我們將逐步進行進行優化。
調整校驗器¶
默認情況下,har2case 生成用例時,若 HTTP 請求的響應內容爲 JSON 格式,則會將第一層級中的所有key-value
轉換爲 validator。
例如上面的第一個測試步驟,生成的 validator 爲:
"validate": [ {"eq": ["status_code", 200]}, {"eq": ["headers.Content-Type", "application/json"]}, {"eq": ["content.success", true]}, {"eq": ["content.token", "baNLX1zhFYP11Seb"]} ]
運行測試用例時,就會對上面的各個項進行校驗。
問題在於,請求/api/get-token
接口時,每次生成的 token 都會是不同的,因此將生成的 token 作爲校驗項的話,校驗自然就無法通過了。
正確的做法是,在測試步驟的 validate 中應該去掉這類動態變化的值。
去除該項後,將用例另存爲 demo-quickstart-1.json(對應的 YAML 格式:demo-quickstart-1.yml)。
再次運行測試用例,運行結果如下:
$ hrun docs/data/demo-quickstart-1.yml INFO Start to run testcase: testcase description /api/get-token INFO POST http://127.0.0.1:5000/api/get-token INFO status_code: 200, response_time(ms): 6.61 ms, response_length: 46 bytes . /api/users/1000 INFO POST http://127.0.0.1:5000/api/users/1000 ERROR 403 Client Error: FORBIDDEN for url: http://127.0.0.1:5000/api/users/1000 ERROR validate: status_code equals 201(int) ==> fail 403(int) equals 201(int) ERROR validate: content.success equals True(bool) ==> fail False(bool) equals True(bool) ERROR validate: content.msg equals user created successfully.(str) ==> fail Authorization failed!(str) equals user created successfully.(str) ERROR ******************************** DETAILED REQUEST & RESPONSE ******************************** ====== request details ====== url: http://127.0.0.1:5000/api/users/1000 method: POST headers: {'Content-Type': 'application/json', 'User-Agent': 'python-requests/2.18.4', 'device_sn': 'FwgRiO7CNA50DSU', 'token': 'baNLX1zhFYP11Seb'} json: {'name': 'user1', 'password': '123456'} verify: True ====== response details ====== status_code: 403 headers: {'Content-Type': 'application/json', 'Content-Length': '50', 'Server': 'Werkzeug/0.14.1 Python/3.7.0', 'Date': 'Sat, 26 Jan 2019 14:45:34 GMT'} body: '{"success": false, "msg": "Authorization failed!"}' F ====================================================================== FAIL: test_0001_000 (httprunner.api.TestSequense) /api/users/1000 ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 54, in test test_runner.run_test(test_dict) httprunner.exceptions.ValidationFailure: validate: status_code equals 201(int) ==> fail 403(int) equals 201(int) validate: content.success equals True(bool) ==> fail False(bool) equals True(bool) validate: content.msg equals user created successfully.(str) ==> fail Authorization failed!(str) equals user created successfully.(str) During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 56, in test self.fail(str(ex)) AssertionError: validate: status_code equals 201(int) ==> fail 403(int) equals 201(int) validate: content.success equals True(bool) ==> fail False(bool) equals True(bool) validate: content.msg equals user created successfully.(str) ==> fail Authorization failed!(str) equals user created successfully.(str) ---------------------------------------------------------------------- Ran 2 tests in 0.018s FAILED (failures=1) INFO Start to render Html report ... INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548513934.html
經過修改,第一個測試步驟已經運行成功了,第二個步驟仍然運行失敗(403),還是因爲權限校驗的原因。
參數關聯¶
我們繼續查看 demo-quickstart-1.json,會發現第二個測試步驟的請求 headers 中的 token 仍然是硬編碼的,即抓包時獲取到的值。在我們再次運行測試用例時,這個 token 已經失效了,所以會出現 403 權限校驗失敗的問題。
正確的做法是,我們應該在每次運行測試用例的時候,先動態獲取到第一個測試步驟中的 token,然後在後續測試步驟的請求中使用前面獲取到的 token。
在 HttpRunner 中,支持參數提取(extract
)和參數引用的功能($var
)。
在測試步驟(test)中,若需要從響應結果中提取參數,則可使用 extract
關鍵字。extract 的列表中可指定一個或多個需要提取的參數。
在提取參數時,當 HTTP 的請求響應結果爲 JSON 格式,則可以採用.
運算符的方式,逐級往下獲取到參數值;響應結果的整體內容引用方式爲 content 或者 body。
例如,第一個接口/api/get-token
的響應結果爲:
{"success": true, "token": "ZQkYhbaQ6q8UFFNE"}
那麼要獲取到 token 參數,就可以使用 content.token 的方式;具體的寫法如下:
"extract": [ {"token": "content.token"} ]
其中,token 作爲提取後的參數名稱,可以在後續使用 $token
進行引用。
"headers": { "device_sn": "FwgRiO7CNA50DSU", "token": "$token", "Content-Type": "application/json" }
修改後的測試用例另存爲 demo-quickstart-2.json(對應的 YAML 格式:demo-quickstart-2.yml)。
再次運行測試用例,運行結果如下:
$ hrun docs/data/demo-quickstart-2.yml INFO Start to run testcase: testcase description /api/get-token INFO POST http://127.0.0.1:5000/api/get-token INFO status_code: 200, response_time(ms): 8.32 ms, response_length: 46 bytes . /api/users/1000 INFO POST http://127.0.0.1:5000/api/users/1000 INFO status_code: 201, response_time(ms): 3.02 ms, response_length: 54 bytes . ---------------------------------------------------------------------- Ran 2 tests in 0.019s OK INFO Start to render Html report ... INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548514191.html
經過修改,第二個測試步驟也運行成功了。
base_url¶
雖然測試步驟運行都成功了,但是仍然有繼續優化的地方。
繼續查看 demo-quickstart-2.json,我們會發現在每個測試步驟的 URL 中,都採用的是完整的描述(host+path),但大多數情況下同一個用例中的 host 都是相同的,區別僅在於 path 部分。
因此,我們可以將各個測試步驟(test) URL 的 base_url
抽取出來,放到全局配置模塊(config)中,在測試步驟中的 URL 只保留 PATH 部分。
- config: name: testcase description base_url: http://127.0.0.1:5000 - test: name: get token request: url: /api/get-token
調整後的測試用例另存爲 demo-quickstart-3.json(對應的 YAML 格式:demo-quickstart-3.yml)。
重啓 flask 應用服務後再次運行測試用例,所有的測試步驟仍然運行成功。
變量的申明和引用¶
繼續查看 demo-quickstart-3.json,我們會發現測試用例中存在較多硬編碼的參數,例如 app_version、device_sn、os_platform、user_id 等。
大多數情況下,我們可以不用修改這些硬編碼的參數,測試用例也能正常運行。但是爲了更好地維護測試用例,例如同一個參數值在測試步驟中出現多次,那麼比較好的做法是,將這些參數定義爲變量,然後在需要參數的地方進行引用。
在 HttpRunner 中,支持變量申明(variables
)和引用($var
)的機制。在 config 和 test 中均可以通過 variables
關鍵字定義變量,然後在測試步驟中可以通過 $ + 變量名稱
的方式引用變量。區別在於,在 config 中定義的變量爲全局的,整個測試用例(testcase)的所有地方均可以引用;在 test 中定義的變量作用域僅侷限於當前測試步驟(teststep)。
對上述各個測試步驟中硬編碼的參數進行變量申明和引用調整後,新的測試用例另存爲 demo-quickstart-4.json(對應的 YAML 格式:demo-quickstart-4.yml)。
重啓 flask 應用服務後再次運行測試用例,所有的測試步驟仍然運行成功。
抽取公共變量¶
查看 demo-quickstart-4.json 可以看出,兩個測試步驟中都定義了 device_sn。針對這類公共的參數,我們可以將其統一定義在 config 的 variables 中,在測試步驟中就不用再重複定義。
- config: name: testcase description base_url: http://127.0.0.1:5000 variables: device_sn: FwgRiO7CNA50DSU
調整後的測試用例見 demo-quickstart-5.json(對應的 YAML 格式:demo-quickstart-5.yml)。
實現動態運算邏輯¶
在 demo-quickstart-5.yml 中,參數 device_sn 代表的是設備的 SN 編碼,雖然採用硬編碼的方式暫時不影響測試用例的運行,但這與真實的用戶場景不大相符。
假設 device_sn 的格式爲 15 長度的字符串,那麼我們就可以在每次運行測試用例的時候,針對 device_sn 生成一個 15 位長度的隨機字符串。與此同時,sign 字段是根據 headers 中的各個字段拼接後生成得到的 MD5 值,因此在 device_sn 變動後,sign 也應該重新進行計算,否則就會再次出現簽名校驗失敗的問題。
然而,HttpRunner 的測試用例都是採用 YAML/JSON 格式進行描述的,在文本格式中如何執行代碼運算呢?
HttpRunner 的實現方式爲,支持熱加載的插件機制(debugtalk.py
),可以在 YAML/JSON 中調用 Python 函數。
具體地做法,我們可以在測試用例文件的同級或其父級目錄中創建一個 debugtalk.py 文件,然後在其中定義相關的函數和變量。
例如,針對 device_sn 的隨機字符串生成功能,我們可以定義一個 gen_random_string 函數;針對 sign 的簽名算法,我們可以定義一個 get_sign 函數。
import hashlib import hmac import random import string SECRET_KEY = "DebugTalk" def gen_random_string(str_len): random_char_list = [] for _ in range(str_len): random_char = random.choice(string.ascii_letters + string.digits) random_char_list.append(random_char) random_string = ''.join(random_char_list) return random_string def get_sign(*args): content = ''.join(args).encode('ascii') sign_key = SECRET_KEY.encode('ascii') sign = hmac.new(sign_key, content, hashlib.sha1).hexdigest() return sign
然後,我們在 YAML/JSON 測試用例文件中,就可以對定義的函數進行調用,對定義的變量進行引用了。引用變量的方式仍然與前面講的一樣,採用$ + 變量名稱
的方式;調用函數的方式爲${func($var)}
。
例如,生成 15 位長度的隨機字符串並賦值給 device_sn 的代碼爲:
"variables": [ {"device_sn": "${gen_random_string(15)}"} ]
使用 $user_agent、$device_sn、$os_platform、$app_version 根據簽名算法生成 sign 值的代碼爲:
"json": { "sign": "${get_sign($user_agent, $device_sn, $os_platform, $app_version)}" }
對測試用例進行上述調整後,另存爲 demo-quickstart-6.json(對應的 YAML 格式:demo-quickstart-6.yml)。
重啓 flask 應用服務後再次運行測試用例,所有的測試步驟仍然運行成功。
參數化數據驅動¶
請確保你使用的 HttpRunner 版本號不低於 2.0.0
在 demo-quickstart-6.yml 中,user_id 仍然是寫死的值,假如我們需要創建 user_id 爲 1001~1004 的用戶,那我們只能不斷地去修改 user_id,然後運行測試用例,重複操作 4 次?或者我們在測試用例文件中將創建用戶的 test 複製 4 份,然後在每一份裏面分別使用不同的 user_id ?
很顯然,不管是採用上述哪種方式,都會很繁瑣,並且也無法應對靈活多變的測試需求。
針對這類需求,HttpRunner 支持參數化數據驅動的功能。
在 HttpRunner 中,若要採用數據驅動的方式來運行測試用例,需要創建一個文件,對測試用例進行引用,並使用 parameters
關鍵字定義參數並指定數據源取值方式。
例如,我們需要在創建用戶的接口中對 user_id 進行參數化,參數化列表爲 1001~1004,並且取值方式爲順序取值,那麼最簡單的描述方式就是直接指定參數列表。具體的編寫方式爲,新建一個測試場景文件 demo-quickstart-7.yml(對應的 JSON 格式:demo-quickstart-7.json),內容如下所示:
config: name: testcase description testcases: create user: testcase: demo-quickstart-6.yml parameters: user_id: [1001, 1002, 1003, 1004]
僅需如上配置,針對 user_id 的參數化數據驅動就完成了。
重啓 flask 應用服務後再次運行測試用例,測試用例運行情況如下所示:
點擊查看運行日誌
可以看出,測試用例總共運行了 4 次,並且每次運行時都是採用的不同 user_id。
關於參數化數據驅動,這裏只描述了最簡單的場景和使用方式,如需瞭解更多,請進一步閱讀《數據驅動使用手冊》。
查看測試報告¶
在每次使用 hrun 命令運行測試用例後,均會生成一份 HTML 格式的測試報告。報告文件位於 reports 目錄下,文件名稱爲測試用例的開始運行時間。
例如,在運行完 demo-quickstart-1.json 後,將生成如下形式的測試報告:
關於測試報告的詳細內容,請查看《測試報告》部分。
總結¶
到此爲止,HttpRunner 的核心功能就介紹完了,掌握本文中的功能特性,足以幫助你應對日常項目工作中至少 80% 的自動化測試需求。
當然,HttpRunner 不止於此,如需挖掘 HttpRunner 的更多特性,實現更復雜場景的自動化測試需求,可繼續閱讀後續文檔。