此文檔適用於全新發布的 HttpRunner 3.x
版本
一、簡介
1.1、介紹
HttpRunner 是一款面向 HTTP(S) 協議的通用測試框架,只需編寫維護一份YAML/JSON腳本,即可實現自動化測試、性能測試、線上監控、持續集成等多種測試需求。
1.2、框架設計理念
- 充分復用優秀的開源項目,不追求重復造輪子,而是將強大的輪子組裝成戰車
- 遵循 約定大於配置 的准則,在框架功能中融入自動化測試最佳工程實踐
- 追求投入產出比,一份投入即可實現多種測試需求
1.3、核心特點
- 繼承 Requests 的全部特性,輕松實現 HTTP(S) 的各種測試需求
- 采用
YAML/JSON
的形式描述測試場景,保障測試用例描述的統一性和可維護性 - 借助輔助函數(debugtalk.py),在測試腳本中輕松實現復雜的動態計算邏輯
- 支持完善的測試用例分層機制,充分實現測試用例的復用
- 測試前后支持完善的 hook 機制
- 響應結果支持豐富的校驗機制
- 使用 jmespath,提取和驗證 json 響應從未如此簡單
- 使用 pytest,數百個插件可供使用
- 基於 HAR 實現接口錄制和用例生成功能(har2case)
- 結合 Locust 框架,無需額外的工作即可實現分布式性能測試
- 執行方式采用 CLI 調用,可與 Jenkins 等持續集成工具完美結合
- 測試結果統計報告簡潔清晰,附帶詳盡統計信息和日志記錄
- 采用 allure,可以生成美觀且強大的測試報告
二、安裝部署
HttpRunner 使用 Python 開發,它支持Python 3.6+ 和大多數操作系統。
2.1 安裝
HttpRunner
已發布到 PyPI
倉庫,可以使用 pip
直接安裝.
pip3 install httprunner
如果要使用最新版本,可以使用 github 倉庫的 URL 安裝。
pip3 install git+https://github.com/httprunner/httprunner.git@master
如果已經安裝過,可以使用 -U
進行升級。
pip3 install -U httprunner
pip3 install -U git+https://github.com/httprunner/httprunner.git@master
2.2 檢查是否安裝成功
如果 HttpRunner 安裝成功,有 4 個命令可用:
httprunner
: 核心命令,可以使用 HttpRunner 的所有命令hrun
:httprunner run
命令的別名,運行 YAML/JSON/pytest 格式的測試用例hmake
:httprunner make
命令的別名,將 YAML/JSON 格式的 testcases 轉換成 pytest 格式的測試用例har2case
:httprunner har2case
命令的別名,將 HAR 文件轉換為 YAML/JSON 格式的測試用例
查看 HttpRunner
版本:
httprunner -V # hrun -V 3.1.0
查看所有可用的命令:
httprunner -h usage: httprunner [-h] [-V] {run,startproject,har2case,make} ... One-stop solution for HTTP(S) testing. positional arguments: {run,startproject,har2case,make} sub-command help run Make HttpRunner testcases and run with pytest. startproject Create a new project with template structure. har2case Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner. make Convert YAML/JSON testcases to pytest cases. optional arguments: -h, --help show this help message and exit -V, --version show version
三、用腳手架快速搭建項目
3.1、快速生成項目
我們不妨先輸入httprunner startproject -h,來看一下命令說明。
httprunner startproject -h
可以看出,只需要在命令后面帶上項目名稱這個參數就好了,那就先來創建一個項目,名稱叫httprunner_demo。
httprunner startproject httprunner_demo
項目生成完畢,也是非常的簡單。
如果你輸入的項目名稱已經存在,httprunner會給出warning提示。

相信了解過django的童鞋能感覺到,httprunner startproject這個命令跟django里的django-admin.py startproject project_name 很像,沒錯,其實httprunner的想法正式來源於django,這就是httprunner作為一個優秀開源技術資源整合和復用的體現之一,后續還有很多,屆時提點出來。
3.2、項目結構梳理
我把生成出的項目丟到sublime里方便查看,可以看的生成的目錄結構如下圖,那么這些都是什么意思呢?
- debugtalk.py 放置在項目根目錄下(借鑒了pytest的conftest文件的設計)
- .env 放置在項目根目錄下,可以用於存放一些環境變量
- reports 文件夾:存儲 HTML 測試報告
- testcases 用於存放測試用例
- har 可以存放錄制導出的.har文件
- 具體用法會在后續中細講,本章不展開。我們可以點開生成的testcases文件夾下的測試用例,里面是提供了一個可運行的demo內容的,那先來運行一下看看。
運行用例:
hrun httprunner_demo
可以看的httprunner輸出了運行過程中的調試信息
最后,運行結束,2個用例運行pass。
前期准備工作就算是結束了,接下來就可以進入到詳細的學習中了。
四、錄制生成測試用例
在正式手動編寫case之前,我們可以先來熟悉下httprunner的錄制生成用例功能。
用postman的童鞋都知道,里面有個功能可以將接口轉換成代碼,可以直接copy過來使用,提升case編寫效率。
那httprunner的錄制生成用例功能又是怎么回事呢?
4.1、har2case
原理就是當前主流的抓包工具和瀏覽器都支持將抓取得到的數據包導出為標准通用的 HAR 格式(HTTP Archive),然后 HttpRunner 將 HAR 格式的數據包轉換為YAML/JSON格式的測試用例文件。
比如,我現在用window系統上的fiddler去抓取一個百度首頁的請求。(常見的Charles,Fiddler、包括瀏覽器自帶的F12開發者工具都可以導出)

選中這個請求,點擊左上角的File——Export Sessions——(可以選擇導出選中的也可以導出所有),這里我們選擇導出選中的,導出HTTPArchive,文件名baidu_home.har,保存到了項目的har目錄下。
4.2 、轉換為pytest文件
獲取.har
文件后,可以使用內置命令har2case
將其轉換為 HttpRunner 測試用例。
har2case 命令幫助:
$ har2case -h usage: har2case har2case [-h] [-2y] [-2j] [--filter FILTER] [--exclude EXCLUDE] [har_source_file] positional arguments: har_source_file 指定 .har 源文件 optional arguments: -h, --help 顯示此幫助信息並退出 -2y, --to-yml, --to-yaml 轉換為 YAML 格式的用例, 如果你沒有特殊指定,默認轉化為 pytest 格式的用例 -2j, --to-json 轉換為 JSON 格式的用例, 如果你沒有特殊指定,默認轉化為 pytest 格式的用例 --filter FILTER 指定過濾關鍵字,只有包含過濾字符串的 url 的 API 才會被轉換 --exclude EXCLUDE 指定忽略關鍵字,如果 url 包含該關鍵字,則會被忽略, 如果需要過濾多個關鍵字,使用`|`(豎線)分隔
運行命令將har文件轉換成測試用例:
har2case baidu_home.har
生成完畢,在har目錄下可以看到生成出的python文件:
# NOTE: Generated By HttpRunner v3.1.1 # FROM: har\baidu_home.har from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseBaiduHome(HttpRunner): config = Config("testcase description").verify(False) teststeps = [ Step( RunRequest("/") .get("https://www.baidu.com/") .with_headers( **{ "Host": "www.baidu.com", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Cookie": "PSTM=1582766561; BAIDUID=5F919C7A22A02E55FBC58E932E7495CD:FG=1; BD_UPN=12314353; BIDUPSID=B2A8970CF5106170D98A137A26C533F7; H_WISE_SIDS=143933_142621_143879_144883_139041_141744_145870_144419_144135_145271_136863_131247_144682_137745_138883_140259_141941_127969_144790_140593_143491_144376_131423_114552_142206_145910_144501_125695_107313_139909_145654_143477_144966_140367_145423_144535_145305_145399_143857_139914_110085; BDUSS=ldaMkhYZjEtLTJQbFhQUTJtU3pwTGhnRVJkeE9YUmNzU2tRRExoZDE3N2hvfmxlSVFBQUFBJCQAAAAAAAAAAAEAAABJufAYUTc2Nzc2MDMyNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEW0l7hFtJeQ; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; delPer=0; BD_CK_SAM=1; PSINO=5; BD_HOME=1; __yjsv5_shitong=1.0_7_47374759f3e4680e7c7afb4a907cb9d2e37d_300_1593325350568_112.80.30.194_b17302a9; yjs_js_security_passport=db59f16989e33eb07a57bd9928ad960ce36495c4_1593325351_js; H_PS_645EC=8043uGVfepN6KMlbDns%2FO0qiazHvEYcE62vhF1luCcNa%2FgwzKQEQq2epcrqdNRvj4iby; H_PS_PSSID=32095_1445_31672_21106_32139_31660_32045_31321; BDSVRTM=0", } ) .with_cookies( **{ "PSTM": "1582766561", "BAIDUID": "5F919C7A22A02E55FBC58E932E7495CD:FG=1", "BD_UPN": "12314353", "BIDUPSID": "B2A8970CF5106170D98A137A26C533F7", "H_WISE_SIDS": "143933_142621_143879_144883_139041_141744_145870_144419_144135_145271_136863_131247_144682_137745_138883_140259_141941_127969_144790_140593_143491_144376_131423_114552_142206_145910_144501_125695_107313_139909_145654_143477_144966_140367_145423_144535_145305_145399_143857_139914_110085", "BDUSS": "ldaMkhYZjEtLTJQbFhQUTJtU3pwTGhnRVJkeE9YUmNzU2tRRExoZDE3N2hvfmxlSVFBQUFBJCQAAAAAAAAAAAEAAABJufAYUTc2Nzc2MDMyNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEW0l7hFtJeQ", "BDORZ": "B490B5EBF6F3CD402E515D22BCDA1598", "BDRCVFR[feWj1Vr5u3D]": "I67x6TjHwwYf0", "delPer": "0", "BD_CK_SAM": "1", "PSINO": "5", "BD_HOME": "1", "__yjsv5_shitong": "1.0_7_47374759f3e4680e7c7afb4a907cb9d2e37d_300_1593325350568_112.80.30.194_b17302a9", "yjs_js_security_passport": "db59f16989e33eb07a57bd9928ad960ce36495c4_1593325351_js", "H_PS_645EC": "8043uGVfepN6KMlbDns%2FO0qiazHvEYcE62vhF1luCcNa%2FgwzKQEQq2epcrqdNRvj4iby", "H_PS_PSSID": "32095_1445_31672_21106_32139_31660_32045_31321", "BDSVRTM": "0", } ) .validate() .assert_equal("status_code", 200) .assert_equal('headers."Content-Type"', "text/html;charset=utf-8") ), ] if __name__ == "__main__": TestCaseBaiduHome().test_start()
因為httprunner封裝了pytest,所有既可以用hrun去運行,也可以用pytest去運行。
hrun
pytest
4.3 轉換為YAML/JSON
很簡單,只要在命令后面多加對應的參數就行了。-2y/--to-yml 或者 -2j/--to-json
轉為YAML:
har2case baidu_home.har -2y
可以查看到轉換生成的yaml文件了。
config: name: testcase description variables: {} verify: false teststeps: - name: / request: cookies: BAIDUID: 5F919C7A22A02E55FBC58E932E7495CD:FG=1 BDORZ: B490B5EBF6F3CD402E515D22BCDA1598 BDRCVFR[feWj1Vr5u3D]: I67x6TjHwwYf0 BDSVRTM: '0' BDUSS: ldaMkhYZjEtLTJQbFhQUTJtU3pwTGhnRVJkeE9YUmNzU2tRRExoZDE3N2hvfmxlSVFBQUFBJCQAAAAAAAAAAAEAAABJufAYUTc2Nzc2MDMyNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEW0l7hFtJeQ BD_CK_SAM: '1' BD_HOME: '1' BD_UPN: '12314353' BIDUPSID: B2A8970CF5106170D98A137A26C533F7 H_PS_645EC: 8043uGVfepN6KMlbDns%2FO0qiazHvEYcE62vhF1luCcNa%2FgwzKQEQq2epcrqdNRvj4iby H_PS_PSSID: '32095_1445_31672_21106_32139_31660_32045_31321' H_WISE_SIDS: '143933_142621_143879_144883_139041_141744_145870_144419_144135_145271_136863_131247_144682_137745_138883_140259_141941_127969_144790_140593_143491_144376_131423_114552_142206_145910_144501_125695_107313_139909_145654_143477_144966_140367_145423_144535_145305_145399_143857_139914_110085' PSINO: '5' PSTM: '1582766561' __yjsv5_shitong: 1.0_7_47374759f3e4680e7c7afb4a907cb9d2e37d_300_1593325350568_112.80.30.194_b17302a9 delPer: '0' yjs_js_security_passport: db59f16989e33eb07a57bd9928ad960ce36495c4_1593325351_js headers: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Connection: keep-alive Cookie: PSTM=1582766561; BAIDUID=5F919C7A22A02E55FBC58E932E7495CD:FG=1; BD_UPN=12314353; BIDUPSID=B2A8970CF5106170D98A137A26C533F7; H_WISE_SIDS=143933_142621_143879_144883_139041_141744_145870_144419_144135_145271_136863_131247_144682_137745_138883_140259_141941_127969_144790_140593_143491_144376_131423_114552_142206_145910_144501_125695_107313_139909_145654_143477_144966_140367_145423_144535_145305_145399_143857_139914_110085; BDUSS=ldaMkhYZjEtLTJQbFhQUTJtU3pwTGhnRVJkeE9YUmNzU2tRRExoZDE3N2hvfmxlSVFBQUFBJCQAAAAAAAAAAAEAAABJufAYUTc2Nzc2MDMyNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEW0l7hFtJeQ; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; delPer=0; BD_CK_SAM=1; PSINO=5; BD_HOME=1; __yjsv5_shitong=1.0_7_47374759f3e4680e7c7afb4a907cb9d2e37d_300_1593325350568_112.80.30.194_b17302a9; yjs_js_security_passport=db59f16989e33eb07a57bd9928ad960ce36495c4_1593325351_js; H_PS_645EC=8043uGVfepN6KMlbDns%2FO0qiazHvEYcE62vhF1luCcNa%2FgwzKQEQq2epcrqdNRvj4iby; H_PS_PSSID=32095_1445_31672_21106_32139_31660_32045_31321; BDSVRTM=0 Host: www.baidu.com Upgrade-Insecure-Requests: '1' User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36 method: GET url: https://www.baidu.com/ validate: - eq: - status_code - 200 - eq: - headers.Content-Type - text/html;charset=utf-8
轉換為JSON:
har2case baidu_home.har -2j
可以看的對應的json文件:
{ "config": { "name": "testcase description", "variables": {}, "verify": false }, "teststeps": [ { "name": "/", "request": { "url": "https://www.baidu.com/", "method": "GET", "cookies": { "PSTM": "1582766561", "BAIDUID": "5F919C7A22A02E55FBC58E932E7495CD:FG=1", "BD_UPN": "12314353", "BIDUPSID": "B2A8970CF5106170D98A137A26C533F7", "H_WISE_SIDS": "143933_142621_143879_144883_139041_141744_145870_144419_144135_145271_136863_131247_144682_137745_138883_140259_141941_127969_144790_140593_143491_144376_131423_114552_142206_145910_144501_125695_107313_139909_145654_143477_144966_140367_145423_144535_145305_145399_143857_139914_110085", "BDUSS": "ldaMkhYZjEtLTJQbFhQUTJtU3pwTGhnRVJkeE9YUmNzU2tRRExoZDE3N2hvfmxlSVFBQUFBJCQAAAAAAAAAAAEAAABJufAYUTc2Nzc2MDMyNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEW0l7hFtJeQ", "BDORZ": "B490B5EBF6F3CD402E515D22BCDA1598", "BDRCVFR[feWj1Vr5u3D]": "I67x6TjHwwYf0", "delPer": "0", "BD_CK_SAM": "1", "PSINO": "5", "BD_HOME": "1", "__yjsv5_shitong": "1.0_7_47374759f3e4680e7c7afb4a907cb9d2e37d_300_1593325350568_112.80.30.194_b17302a9", "yjs_js_security_passport": "db59f16989e33eb07a57bd9928ad960ce36495c4_1593325351_js", "H_PS_645EC": "8043uGVfepN6KMlbDns%2FO0qiazHvEYcE62vhF1luCcNa%2FgwzKQEQq2epcrqdNRvj4iby", "H_PS_PSSID": "32095_1445_31672_21106_32139_31660_32045_31321", "BDSVRTM": "0" }, "headers": { "Host": "www.baidu.com", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Cookie": "PSTM=1582766561; BAIDUID=5F919C7A22A02E55FBC58E932E7495CD:FG=1; BD_UPN=12314353; BIDUPSID=B2A8970CF5106170D98A137A26C533F7; H_WISE_SIDS=143933_142621_143879_144883_139041_141744_145870_144419_144135_145271_136863_131247_144682_137745_138883_140259_141941_127969_144790_140593_143491_144376_131423_114552_142206_145910_144501_125695_107313_139909_145654_143477_144966_140367_145423_144535_145305_145399_143857_139914_110085; BDUSS=ldaMkhYZjEtLTJQbFhQUTJtU3pwTGhnRVJkeE9YUmNzU2tRRExoZDE3N2hvfmxlSVFBQUFBJCQAAAAAAAAAAAEAAABJufAYUTc2Nzc2MDMyNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEW0l7hFtJeQ; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; delPer=0; BD_CK_SAM=1; PSINO=5; BD_HOME=1; __yjsv5_shitong=1.0_7_47374759f3e4680e7c7afb4a907cb9d2e37d_300_1593325350568_112.80.30.194_b17302a9; yjs_js_security_passport=db59f16989e33eb07a57bd9928ad960ce36495c4_1593325351_js; H_PS_645EC=8043uGVfepN6KMlbDns%2FO0qiazHvEYcE62vhF1luCcNa%2FgwzKQEQq2epcrqdNRvj4iby; H_PS_PSSID=32095_1445_31672_21106_32139_31660_32045_31321; BDSVRTM=0" } }, "validate": [ { "eq": [ "status_code", 200 ] }, { "eq": [ "headers.Content-Type", "text/html;charset=utf-8" ] } ] } ] }
以上轉換出的pytest、yaml、json這3種格式的文件效果都是一樣的,用hrun都可以運行,但是用pytest執行的話只可以運行.py的文件了。
五、結構解析
HttpRunner v3.x 支持三種測試用例格式,即 pytest,YAML 和 JSON。
格式關系如下圖所示:

上圖是來自官方的用例格式關系圖,可以看出來,httprunner再對於第三方導出的har文件進行了轉換處理,有的人喜歡轉換成json,有的人喜歡轉換成yaml。但是最終,還是通過解析json格式的文件,生成pytest的python文件。
既然最后都是要生成pytest,那何不一步到位呢?哈哈,我想這就是官方推薦pytest格式的原因吧。
我還是挺喜歡的,因為我對於pytest使用的較多,那么接下來也是基於pytest格式的用例進行解析。
5.1 用例結構
每個測試用例都是 HttpRunner 的子類(一個類即為一個測試用例),並且必須具有兩個類屬性:config
和 teststeps
。
-
config
:配置測試用例級別的設置,包括 base_url,verify,variables,export。 -
teststeps
:測試步驟的列表(List [Step]
),每個步驟對應一個 API 請求或另一個測試用例的應用。此外,還支持variables
/extract
/validate
/hooks
來創建極其復雜的測試方案。
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseRequestWithFunctions(HttpRunner): config = ( Config("request methods testcase with functions") .variables( **{ "foo1": "config_bar1", "foo2": "config_bar2", "expect_foo1": "config_bar1", "expect_foo2": "config_bar2", } ) .base_url("https://postman-echo.com") .verify(False) .export(*["foo3"]) ) teststeps = [ Step( RunRequest("get with params") .with_variables( **{"foo1": "bar11", "foo2": "bar21", "sum_v": "${sum_two(1, 2)}"} ) .get("/get") .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}) .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) .extract() .with_jmespath("body.args.foo2", "foo3") .validate() .assert_equal("status_code", 200) .assert_equal("body.args.foo1", "bar11") .assert_equal("body.args.sum_v", "3") .assert_equal("body.args.foo2", "bar21") ), Step( RunRequest("post form data") .with_variables(**{"foo2": "bar23"}) .post("/post") .with_headers( **{ "User-Agent": "HttpRunner/${get_httprunner_version()}", "Content-Type": "application/x-www-form-urlencoded", } ) .with_data("foo1=$foo1&foo2=$foo2&foo3=$foo3") .validate() .assert_equal("status_code", 200) .assert_equal("body.form.foo1", "$expect_foo1") .assert_equal("body.form.foo2", "bar23") .assert_equal("body.form.foo3", "bar21") ), ] if __name__ == "__main__": TestCaseRequestWithFunctions().test_start()
5.2 鏈式調用


5.3、httprunner的用例結構與我自己的用例
補一個官方完整的一個demo代碼,並說說httprunner中的用例與我自己編寫的測試用例之間的聯系。
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseRequestWithFunctions(HttpRunner): config = ( Config("request methods testcase with functions") .variables( **{ "foo1": "config_bar1", "foo2": "config_bar2", "expect_foo1": "config_bar1", "expect_foo2": "config_bar2", } ) .base_url("http://demo.qa.com") .verify(False) .export(*["foo3"]) ) teststeps = [ Step( RunRequest("get with params") .with_variables( **{"foo1": "bar11", "foo2": "bar21", "sum_v": "${sum_two(1, 2)}"} ) .get("/get") .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}) .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) .extract() .with_jmespath("body.args.foo2", "foo3") .validate() .assert_equal("status_code", 200) .assert_equal("body.args.foo1", "bar11") .assert_equal("body.args.sum_v", "3") .assert_equal("body.args.foo2", "bar21") ), Step( RunRequest("post form data") .with_variables(**{"foo2": "bar23"}) .post("/post") .with_headers( **{ "User-Agent": "HttpRunner/${get_httprunner_version()}", "Content-Type": "application/x-www-form-urlencoded", } ) .with_data("foo1=$foo1&foo2=$foo2&foo3=$foo3") .validate() .assert_equal("status_code", 200) .assert_equal("body.form.foo1", "$expect_foo1") .assert_equal("body.form.foo2", "bar23") .assert_equal("body.form.foo3", "bar21") ), ] if __name__ == "__main__": TestCaseRequestWithFunctions().test_start()
- httprunner中的testcase,其實說的就是上面的這一整個Python文件。
- teststeps列表中的Step,其實就是我自己編寫case時候的一個個def test_xxx():pass。
- 而每一個Step內部,依然是按照 傳參——調用接口——斷言,這樣的過程來的。
萬變不離其宗,httprunner框架目前看起來,確實可以讓編寫更加的便捷、簡潔,但是這只是目前從demo的過程中得到的結論,后面還需要落地實戰才可以。
六、測試用例 - config
上一篇中,我們了解到了config,在配置中,我們可以配置測試用例級級別的一些設置,比如基礎url、驗證、變量、導出。
我們一起來看,官方給出的一個例子:
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseRequestWithFunctions(HttpRunner): config = ( Config("request methods testcase with functions") .variables( **{ "foo1": "config_bar1", "foo2": "config_bar2", "expect_foo1": "config_bar1", "expect_foo2": "config_bar2", } ) .base_url("http://demo.qa.com") .verify(False) .export(*["foo3"]) ) teststeps = [ Step( RunRequest("get with params") .with_variables( **{"foo1": "bar11", "foo2": "bar21", "sum_v": "${sum_two(1, 2)}"} ) .get("/get") .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}) .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) .extract() .with_jmespath("body.args.foo2", "foo3") .validate() .assert_equal("status_code", 200) .assert_equal("body.args.foo1", "bar11") .assert_equal("body.args.sum_v", "3") .assert_equal("body.args.foo2", "bar21") ), Step( RunRequest("post form data") .with_variables(**{"foo2": "bar23"}) .post("/post") .with_headers( **{ "User-Agent": "HttpRunner/${get_httprunner_version()}", "Content-Type": "application/x-www-form-urlencoded", } ) .with_data("foo1=$foo1&foo2=$foo2&foo3=$foo3") .validate() .assert_equal("status_code", 200) .assert_equal("body.form.foo1", "$expect_foo1") .assert_equal("body.form.foo2", "bar23") .assert_equal("body.form.foo3", "bar21") ), ] if __name__ == "__main__": TestCaseRequestWithFunctions().test_start()
6.1、name(必填)
即用例名稱,這是一個必填參數。測試用例名稱,將顯示在執行日志和測試報告中。比如,我在之前的百度搜索的case里,加入name。

運行后,在debug日志里,可以看的用例名稱被展示出來。
6.2、base_url(選填)
其實這個配置一般在多環境切換中最常用。
比如你的這套測試用例在qa環境,uat環境都要使用,那么就可以把基礎地址(舉例http://demo.qa.com),設置進去。在后面的teststep中,只需要填上接口的相對路徑就好了(舉例 /get)。
這樣的話,切換環境運行,只需要修改base_url即可。
6.3、variables(選填)
變量,這里可以存放一些公共的變量,可以在測試用例里引用。這里大家可以記住這個“公共”的詞眼,因為在后面的Step中,還會有步驟變量。
比如說,我的接口有個傳參是不變的,比如用戶名username,而且后面的沒個Step都會用到這個傳參,那么username就可以放在config的公共變量里。
另外,Step里的變量優先級是比config里的變量要高的,如果有2個同名的變量的話,那么引用的時候,是優先引用步驟里的變量的。
6.4、verify(選填)
用來決定是否驗證服務器TLS證書的開關。
通常設置為False,當請求https請求時,就會跳過驗證。如果你運行時候發現拋錯SSLError,可以檢查一下是不是verify沒傳,或者設置了True。
SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1076)'))
6.5、export(選填)
導出的變量,主要是用於Step之間參數的傳遞。還是以上面的官方代碼為例:
- 在config中配置export“foo3”這個變量。
- 在第一個Step中,.extract() 提取了"body.args.foo2"給變量“foo3”。
- 在第二個Step中,引用變量"foo3"。
像參數傳遞,提取這些點,會放在后面單獨講解,前面還是以熟悉框架為主。
七、 測試用例-teststeps-RunRequest
之前我們了解了config里的各項參數,今天來了解另一個重要部分——teststeps,在這之前,先看看測試用例的分層模型。
7.1、測試用例分層模型
一個testcase里(就是一個pytest格式的Python文件)可以有一個或者多個測試步驟,就是teststeps[]列表里的Step。
我的理解每一個Step就可以類比成pytest框架下的def test_xxx()的用例函數,在Step里通常都會要請求API完成測試,也可以調用其他測試用例來完成更多的需求。
可以來看下官方的測試用例邏輯圖(2.x版本不同,3.x棄用了2.x的API概念):

可以看到,testsuite包含了testcase,testcase1需要依賴testcase2才可以完成,那么就可以在teststep12對其進行引用;而testcase2又依賴於testcase3,那么也可以在teststep22進行引用。
但是在整個testsuite下,這3個testcase都是相互獨立的,可以獨自運行。如果需要相互調用,則是在testcase內部去完成處理。
可能看起來有點繞,其實官方想表達的就是測試用例分層的一個思想:
- 測試用例(testcase)應該是完整且獨立的,每條測試用例應該是都可以獨立運行的
- 測試用例是測試步驟(teststep)的有序集合
- 測試用例集(testsuite)是測試用例的無序集合,集合中的測試用例應該都是相互獨立,不存在先后依賴關系的;如果確實存在先后依賴關系,那就需要在測試用例中完成依賴的處理
其實這一點,在我們自己使用pytest框架編寫測試用例的時候同樣貫徹到了。為了自動化測試的穩定性和可維護性,每個測試用例之間相互獨立是非常有必要的。
7.2、teststeps-RunRequest
先上一段Step的代碼,結合下面的點對照着看:
teststeps = [ Step( RunRequest("get with params") .with_variables( **{"foo1": "bar11", "foo2": "bar21", "sum_v": "${sum_two(1, 2)}"} ) .get("/get") .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}) .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) .extract() .with_jmespath("body.args.foo2", "foo3") .validate() .assert_equal("status_code", 200) .assert_equal("body.args.foo1", "bar11") .assert_equal("body.args.sum_v", "3") .assert_equal("body.args.foo2", "bar21") ),
從上面的代碼可以看出,RunRequest的作用就是在測試步驟中請求API,並且可以對於API的響應結果進行提取、斷言。
1.RunRequest(name)
RunRequest的參數名用於指定teststep名稱,它將顯示在執行日志和測試報告中。
2. .with_variables
用於存放變量,但是這里的是Step步驟里的變量,不同的Step的變量是相互獨立的。所以對於多個Step都要使用的變量,我們可以放到config的變量里去。
另外,如果config和Step里有重名的變量,那么當你引用這個變量的時候,Step變量會覆蓋config變量。
3. .method(url)
這里就是指定請求API的方法了,常用的get、post等等。如圖所示,就是用的get方法,括號里的url就是要請求的地址了。
這里要注意的是,如果在config里設置了基礎url,那么步驟里的url就只能設置相對路徑了。

4. .with_params
這個就簡單了,測接口不都得要傳參么,對於params類型的傳參,就放這就行了,key-value鍵值對的形式。對於body形式的傳參,看后面。

5. .with_headers
同樣,有header要帶上的就放這里。

需要帶cookie的,可以用.with_cookies方法。

7. .with_data
對於body類型的傳參,可以用.with_data。

8. .with_json
如果是json類型的body請求體,可以用.with_json。

9. .extract
這里就是要做提取操作了,使用.with_jmespath(jmes_path: Text, var_name: Text)。
這里是采用了JMESPath語言,JMESPath是JSON的查詢語言,可以便捷的提取json中你需要的元素。
第一個參數是你的目標元素的jmespath表達式,第二個元素則是用來存放這個元素的變量,供有需要的引用。
這里不展開,后面單講。

10. .validate
斷言,我們測試最終就是要驗證接口返回是否符合預期。
那在httprunner框架中,可以使用assert_XXX(jmes_path: Text, expected_value: Any)來進行提取和驗證。
第一個參數還是jmespath表達式,第二個參數則是預期值。
assert_XXX這種方式相信用過自動化測試框架的都不會陌生,所以也非常容易上手。目前httprunner還是封裝了非常豐富的斷言方法的,相信可以滿足絕大多數的需求了。
- equals: 是否相等
- less_than: 小於
- less_than_or_equals: 小於等於
- greater_than: 大於
- greater_than_or_equals: 大於等於
- not_equals: 不等於
- string_equals: 字符串相等
- length_equals: 長度相等
- length_greater_than: 長度大於
- length_greater_than_or_equals: 長度大於等於
- length_less_than: 長度小於
- length_less_than_or_equals: 長度小於等於
- contains: 預期結果是否被包含在實際結果中
- contained_by: 實際結果是否被包含在預期結果中
- type_match: 類型是否匹配
- regex_match: 正則表達式是否匹配
- startswith: 字符串是否以什么開頭
- endswith: 字符串是否以什么結尾
八、測試用例-teststeps-RunTestCase
以前我在寫接口自動化用例的時候,為了保證用例的獨立性,需要在setUp里調用各種滿足用例的一些前置條件,其中就不乏調用了其他測試用例中的方法。
而httprunner也是支持了這一項很重要的特性,通過RunTestCase對其他測試用例進行調用,並且還可以導出用例中你所需要的變量,來滿足后續用例的的運行。
首先還是來看下RunTestCase的用法,然后再用實例去實踐。
teststeps = [ Step( RunTestCase("request with functions") .with_variables( **{"foo1": "testcase_ref_bar1", "expect_foo1": "testcase_ref_bar1"} ) .call(RequestWithFunctions) .export(*["foo3"]) ), Step( RunRequest("post form data") .with_variables(**{"foo1": "bar1"}) .post("/post") .with_headers( **{ "User-Agent": "HttpRunner/${get_httprunner_version()}", "Content-Type": "application/x-www-form-urlencoded", } ) .with_data("foo1=$foo1&foo2=$foo3") .validate() .assert_equal("status_code", 200) .assert_equal("body.form.foo1", "bar1") .assert_equal("body.form.foo2", "bar21") ),
8.1 . RunTestCase(name)
這個參數呢還是一個名稱,畢竟RunTestCase還是一個Step,這個名稱同樣會在日志和報告中顯示。

8.2. .with_variables
這個變量跟RunRequest里的用法一樣。
8.3. .call
這里就是指定你要引用的testcase類名稱了。

8.4. .export
可以指導出的變量,以供后續Step引用。
可以看的.export()內部是一個列表[],這里可以用來導出多個變量。
九、用例引入,變量傳遞
進行一些實踐操作。
上一篇提到了RunTestCase,里面有2個重要的特征:
一個是在一個用例中引用另一個測試用例,另一個則是變量的導出與引用。
用flask快速寫了2個接口,以供在本地調用:
from flask import Flask from flask import request app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello World!' @app.route('/getUserName', methods=['GET']) def get_user_name(): if request.method == 'GET': return { "username": "wesson", "age": "27", "from": "China", } @app.route('/joinStr', methods=['GET']) def str_join(): if request.method == 'GET': str1 = request.args.get("str1") str2 = request.args.get("str2") after_join = str1 + " " + str2 return { "result": after_join } if __name__ == '__main__': app.run()
一共有2個接口:
- /getUserName,查詢用戶名,返回是我寫死的字典。
- /joinStr,兩個字符串拼接,返回的是拼接后的結果。
9.1、編寫測試用例
根據之前學習過的,直接編寫case,因為這個接口沒有傳參,cookie之類的,就省掉了,只是demo用。
1. 接口:/getUserName
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseRequestWithGetUserName(HttpRunner): config = ( Config("test /getUserName") .base_url("http://localhost:5000") .verify(False) ) teststeps = [ Step( RunRequest("getUserName") .get("/getUserName") .validate() .assert_equal("body.username", "wesson") ), ] if __name__ == "__main__": TestCaseRequestWithGetUserName().test_start()
這里呢,步驟都有了,斷言是驗證返回的username字段值是不是“wesson”,運行一下,可以看到測試通過。
2. 接口:/joinStr
這個接口就需要2個傳參了,那么在Step里通過.with_params()來傳參。
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseRequestWithJoinStr(HttpRunner): config = ( Config("test /joinStr") .base_url("http://localhost:5000") .verify(False) ) teststeps = [ Step( RunRequest("joinStr") .get("/joinStr") .with_params(**{"str1": "hello", "str2": "wesson"}) .validate() .assert_equal("body.result", "hello wesson") ), ] if __name__ == "__main__": TestCaseRequestWithJoinStr().test_start()
這里傳入的參數分別是“hello”、“wesson”,這個字符串在拼接的時候是加了一個空格的,所以斷言的時候我預期的值是"hello wesson"。
運行測試,可以看的測試通過。

9.2、testcase引用&變量傳遞
以上是2個分開的case,都可以分別正常運行。
假設,/joinStr接口的第二個參數,是依賴/getUserName接口的返回,那么現在這2個testcase之間就有了依賴關系。
那么在寫/getUserName接口用例的時候,就需要去引用/joinStr的測試用例了,並且需要把/getUserName用例的變量導出來,/joinStr的測試用例傳參時候使用。
1. 首先,先修改/getUserName接口的case:
from httprunner import HttpRunner, Config, Step, RunRequest class TestCaseRequestWithGetUserName(HttpRunner): config = ( Config("test /getUserName") .base_url("http://localhost:5000") .verify(False) .export(*["username"])#這里定義出要導出的變量 ) teststeps = [ Step( RunRequest("getUserName") .get("/getUserName") .extract() .with_jmespath("body.username", "username")#提取出目標值,賦值給username變量 .validate() .assert_equal("body.username", "wesson") ), ] if __name__ == "__main__": TestCaseRequestWithGetUserName().test_start()
關注注釋部分的代碼,一個是config里定義了這個要導出的變量,另一個是在Step中,講目標值提取出來,賦值給這個變量。
2. 接下來,修改/joinStr接口的測試用例:
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from .get_user_name_test import TestCaseRequestWithGetUserName #記得要導入引用的類 class TestCaseRequestWithJoinStr(HttpRunner): config = ( Config("test /joinStr") .base_url("http://localhost:5000") .verify(False) ) teststeps = [ Step( RunTestCase("setUp getUserName") .call(TestCaseRequestWithGetUserName)#導入后就可以調用了 .export(*["username"])#在RunTestCase步驟中定義這個變量的導出 ), Step( RunRequest("joinStr") .get("/joinStr") .with_params(**{"str1": "hello", "str2": "$username"})#在第二個傳參中引用導出的變量 .validate() .assert_equal("body.result", "hello $username")#斷言的預期值也引用變量 ), ] if __name__ == "__main__": TestCaseRequestWithJoinStr().test_start()
按照直接學習的內容,case已經修改好,現在運行/joinStr接口的測試用例,可以看到運行通過。
10、報告生成
HTTPrunner 集成了 pytest,所以 HTTPrunner v3.x 可以使用 pytest 的所有插件,包括測試報告插件,例如pytest-html
和
alluer-pytest
。
10.1 HTML 測試報告
HTTPrunner 安裝之后自帶 pytest-html
插件,當你想生成 HTML 測試報告時,可以添加命令參數--html
。
$ hrun /path/to/testcase --html=report.html
-
--html=report.html中的report.html是測試報告的存放路徑,沒有帶文件夾的時候會存放在命令運行的文件夾。
通過--html
生成的 HTML 報告包含了多個文件夾,如果你要創建一個單獨的 HTML 文件,可以添加另一個命令參數--self-contained-html
。
$ hrun /path/to/testcase --html=report.html --self-contained-html
你可以參考pytest-html,獲取更多關於 pytest-html 測試報告的信息。
10.2 allure report
pytest 支持大名鼎鼎的 allure 測試報告,HTTPrunner 集成了 pytest,也天然支持 allure。
不過 HTTPrunner 默認並未安裝 allure,你需要另外安裝。
安裝有兩種方式:
- 安裝 allure 的 pytest 依賴庫
allure-pytest
; - 安裝 HTTPrunner 的 allure 依賴庫
httprunner[allure]
。
安裝 allure-pytest
:
$ pip3 install "allure-pytest"
安裝 httprunner[allure]
(推薦)
pip3 install "httprunner[allure]"
一旦 allure-pytest 准備好,以下參數就可以與 hrun/pytest 命令一起使用:
--alluredir=DIR
: 生成 allure 報告到指定目錄
--clean-alluredir
: 如果指定目錄已存在則清理該文件夾
--allure-no-capture
:不要將 pytest 捕獲的日志記錄(logging)、標准輸出(stdout)、標准錯誤(stderr)附加到報告中
要使 Allure 偵聽器能夠在測試執行期間收集結果,只需添加--alluredir
選項,並提供指向存儲結果的文件夾路徑。如:
$ hrun /path/to/testcase --alluredir=/tmp/my_allure_results
/tmp/my_allure_results 只會存儲收集的測試結果(你將會看到一堆莫名其妙的文件,這些都是收集的測試結果),並非完成的報告,還需要通過命令生成。
要在測試完成后查看實際報告,您需要使用Allure命令行實用程序從結果中生成報告。
$ allure serve /tmp/my_allure_results
此命令將在默認瀏覽器中顯示你生成的報告。
同時你也可以在 Jenkins 中安裝 allure 報告插件,將結果與 Jenkins 集成。
你可以在 allure-pytest 查看關於 allure 報告的更多信息。