Httprunner
1.简介
面向HTTP(S)协议的测试通用框架,维护YAML/JSON脚本执行测试用例,最终都是转化为python文件执行,3.0以后官方建议直接转为维护python脚本
特性
- 继承Requests特性
- 辅助函数debugtalk.py,实现动态计算逻辑
- 测试分层,api层、测试用例层、测试套件
- 支持Hook机制
- 丰富的校验机制
- 基于HAR实现接口录制和用例生成功能
- 结合locust框架,分布式性能测试
- 可与jenkins进行持续集成
- 支持测试报告,pytest-html和allure
- 可拓展,支持二次开发和平台化
文件
- YAML/JSON,测试用例文件,一个文件对应一条测试用例
- debugtalk.py,脚本函数,存在时所在目录被视为项目工程的根目录,不存在时运行测试的路径为根目录,测试用例文件和测试报告文件都是基于该目录运行和生成
- .env,存储项目全局变量
- .csv 项目数据文件,用于数据驱动
- report(自动生成) 运行后自动生成,无需创建
- testcase 存放测试用例
- har 存放导出的文件
2.YAML文件简介
-
:表示键值对
-
-表示数组
-
纯量:字符串、整数、浮点数、布尔值、NULL、时间、日期
-
对象,数组嵌套
- id: 1name: kong -id: 2name: zhenguocity: - 'shandong'- 'jinan'
3. 环境安装
pip install httprunner==版本号
hrun -v
查看版本号
httprunner startproject httprunner_demo
创建项目
hrun httprunner_demo
运行
4.快速生成接口测试用例
-
fiddler获取接口.har包
选中单接口或多接口,File->Export Sessions->selected session->选择HTTPArchive v1.2
-
har转yaml
har2case name.har -2y
-
har转json
har2cae name.har -2j
-
har转python
har2case name.bar
-
执行:yaml和json 直接hrun name.yaml,python文件也可以使用pytest
5.脚本文件详解
yaml
-
get
- config: # 配置信息name: '测试' # 测试用例名称base_url: 'url' # ip地址- test: name: '第一步测试' # 测试步骤名称request: url: '/login' # 路径method: GETheaders:Accept: 'text/html'params:name: ''age: ''validate: # 断言- eq: ['status_code',200]- eq: [content.expires_in,7200] # content表示接口响应的json
-
post
- config: # 配置信息name: '测试' # 测试用例名称base_url: 'url' # ip地址- test: name: '第一步测试' # 测试步骤名称request: url: '/login' # 路径method: POSTheaders:Content-Type: 'application/json'json:{"name": {"age": 18}}validate: # 断言- eq: ['status_code',200]- eq: [content.expires_in,7200] # content表示接口响应的json
python
-
get
# NOTE: Generated By HttpRunner v3.1.4 # FROM: har/baidu_home.harfrom httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase# 一个类是一个testcase,继承自HTTpRunner class TestCaseBaiduHome(HttpRunner):config = Config("testcase description").verify(False).base-url('ip地址').variables(**{"name": "123"}).export(*["token", "list"])# 配置测试用例设置,包括url、验证、变量、导出"""Config(): 显示在执行日志和测试报告中base_url() 主机ip,用于切换测试环境variables() 公共变量,级别比Step中的低,同名不执行verify 用来决定是否验证服务器TLS证书的开关。通常设置为False,当请求https请求时,就会跳过验证。如果你运行时候发现抛错SSLError,可以检查一下是不是verify没传,或者设置了True。export() 导出的变量** 解构字典,*结构列表或元组"""teststeps = [# teststeps 每个Step对应一个API请求,也可以调用另一个testcaseStep(RunRequest("/s") # 指定测试用例名称,显示在日志和测试报告中.get("https://www.baidu.com/s").call(导入的测试用例名称) # 导入后可使用其中的变量.with_variables(**{"foo1": "testcase_ref_bar1", "expect_foo1": "testcase_ref_bar1"}) # 测试用例变量,会覆盖全局变量中重名的变量.with_params(**{"ie": "utf-8","mod": "1","isbd": "1","isid": "C9FF25725AB54698","f": "8","rsv_bp": "1","rsv_idx": "1","tn": "baidu","wd": "httprunner","fenlei": "256","oq": "httprunner%20%26lt%3B","rsv_pq": "86a39119000039fe","rsv_t": "9d65Kx91ldJ2V3LDLjZmstZTQ27dNOYAMJ++oE9TlK6y2o+O7A7XdDS6Yus","rqlang": "cn","rsv_enter": "0","rsv_dl": "tb","rsv_sug3": "2","rsv_sug1": "2","rsv_sug7": "000","rsv_btype": "t","prefixsug": "httprunner","rsp": "1","inputT": "6648","rsv_sug4": "7252","rsv_sug": "2","bs": "httprunner 3","rsv_sid": "undefined","_ss": "1","clist": "","hsug": "httprunner 3\thttprunner","f4s": "1","csor": "10","_cr1": "40730",}).with_headers(**{"Host": "www.baidu.com","Connection": "keep-alive","Accept": "*/*","is_xhr": "1","X-Requested-With": "XMLHttpRequest","is_referer": "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=httprunner%203&fenlei=256&oq=httprunner%203&rsv_pq=86a39119000039fe&rsv_t=2b6c1PBdGIcDYzEKyW9BkMzeCPMYcfbqTSf%2FEDXZuefGUrmcy2q1pxhJ0NQ&rqlang=cn","User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36","Sec-Fetch-Site": "same-origin","Sec-Fetch-Mode": "cors","Sec-Fetch-Dest": "empty","Referer": "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=httprunner&fenlei=256&oq=httprunner%2520%2526lt%253B&rsv_pq=86a39119000039fe&rsv_t=9d65Kx91ldJ2V3LDLjZmstZTQ27dNOYAMJ%2B%2BoE9TlK6y2o%2BO7A7XdDS6Yus&rqlang=cn&rsv_enter=0&rsv_dl=tb&rsv_sug3=2&rsv_sug1=2&rsv_sug7=000&rsv_btype=t&prefixsug=httprunner&rsp=1&inputT=6648&rsv_sug4=7252&rsv_sug=2","Accept-Encoding": "gzip, deflate, br","Accept-Language": "zh-CN,zh;q=0.9","Cookie": "BIDUPSID=EA49B0E234E0F93BBD3C0082A586CDEA; PSTM=1619952293; BAIDUID=C9FF25B24E5A3C59C96D61DB506725AB:FG=1; BD_UPN=123253; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; H_PS_PSSID=33986_33819_33849_33759_33675_33607_26350_33996; H_PS_645EC=9d65Kx91ldJ2V3LDLjZmstZTQ27dNOYAMJ%2B%2BoE9TlK6y2o%2BO7A7XdDS6Yus; delPer=0; BD_CK_SAM=1; PSINO=5; BDSVRTM=14; WWW_ST=1620121549937",}).with_cookies(**{"BIDUPSID": "EA49B0E234E0F93BBD3C0082A586CDEA","PSTM": "1619952293","BAIDUID": "C9FF25B24E5A3C59C96D61DB506725AB:FG=1","BD_UPN": "123253","BDORZ": "B490B5EBF6F3CD402E515D22BCDA1598","H_PS_PSSID": "33986_33819_33849_33759_33675_33607_26350_33996","H_PS_645EC": "9d65Kx91ldJ2V3LDLjZmstZTQ27dNOYAMJ%2B%2BoE9TlK6y2o%2BO7A7XdDS6Yus","delPer": "0","BD_CK_SAM": "1","PSINO": "5","BDSVRTM": "14","WWW_ST": "1620121549937",}).validate().assert_equal("status_code", 200).assert_equal('headers."Content-Type"', "text/html;charset=utf-8")),]if __name__ == "__main__":TestCaseBaiduHome().test_start()
6.获得响应数据&extract提取值到变量
yaml
-
提取响应头、响应行
- test:name: 接口名称 百度接口request:url: /method: GETextract: # 提取值存储到变量中- code: status_code # 响应码- info: reason # ok- header_Content: headers.Content-Type # 响应头部validate:- eq: [$code,200] # 引用变量 $变量名- eq: [$info,"OK"]- eq: [$header_Content,'text/html']
-
正则解析相应内容
- test:name: 百度主页request:url: /method: GETheaders: # 如果断言为中文的话,加上headers的Accept-Language即可Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36extract:- title: <title>(.+?)</title> # 可以使用正则表达式提取validate:- eq: [$title,"百度一下,你就知道"]
-
解析响应正文
- test:name: 百度主页request:url: /cgi-bin/tags/getmethod: GETparams:access_token: 49_lsdk_pQJJ4R5IWdWVcDTQu3bHyVOsHDlAcuA99UtVwsmzrtHhSGJKgSPMi3i3TdOQrGeuzZdB62K1uhcKJQAk6eKjzlBL7HgWvAmw7gfiRTp00QnLdSZzN7ul9f2TMPex-Iz2tCg-ZWsSPLbJTJdABAYIYextract:- id: content.tags.0.id # content为根节点- name: content.tags.0.namevalidate:- eq: [$id,2]- eq: [$name,"星标组"]
python
-
Step(RunTestCase("request with functions").with_variables(**{"foo1": "testcase_ref_bar1", "expect_foo1": "testcase_ref_bar1"}).call(RequestWithFunctions).extract().with_jmespath("body.args.foo2", "foow") # json提取)
7.接口关联
当前文件
-
yaml
- test:name: 获取tokenrequest:url: /cgi-bin/tokenmethod: GETparams:grant_type: client_credentialappid: wxf144190secret: 92a113bd4b5extract: # 提取变量- token: content.access_token- time: content.expires_invalidate:- eq: [$time,7200]- test:name: 获取用户所有标签request:url: /cgi-bin/tags/getmethod: GETparams:access_token: $token # 引用上面的token实现关联extract:- id: content.tags.0.id- name: content.tags.0.namevalidate:- eq: [$id,2]- eq: [$name,"星标组"]
-
python
Step(RunTestCase("request with functions").with_variables(**{"foo1": "testcase_ref_bar1", "expect_foo1": "testcase_ref_bar1"}).call(RequestWithFunctions).extract().with_jmespath("body.args.foo2", "foow") # json提取)Step(RunTestCase("request with functions").with_variables(**{"foo1": "testcase_ref_bar1", "expect_foo1": "testcase_ref_bar1"}).with_params(**{"str1": "hello", "str2": "$foow"}) # 在第二个传参中引用导出的变量)
跨文件
-
yaml
test.yml
- config: # 配置信息name: '测试' # 测试用例名称base_url: 'url' # ip地址export: - token- test: name: '第一步测试' # 测试步骤名称request: url: '/login' # 路径method: GETheaders:Accept: 'text/html'params:name: ''age: ''validate: # 断言- eq: ['status_code',200]- eq: [content.expires_in,7200] # content表示接口响应的jsonextract:token: content.access_token
- config: # 配置信息name: '测试' # 测试用例名称base_url: 'url' # ip地址- test: name: '第一步测试' # 测试步骤名称testcase: test.yamlextract:- tokenrequest: url: '/login' # 路径method: GETheaders:Accept: 'text/html'params:name: ''age: ''validate: # 断言- eq: ['status_code',200]- eq: [content.expires_in,7200] # content表示接口响应的json- eq: [content, $token]
-
python
testcases.test_getUserName_demo
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCaseclass 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", "chenshifeng")),]if __name__ == "__main__":TestCaseRequestWithGetUserName().test_start()
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from testcases.test_getUserName_demo import TestCaseRequestWithGetUserName as RequestWithGetUserName # 记得要导入引用的类class TestCaseRequestWithJoinStr(HttpRunner):config = (Config("test /joinStr").base_url("http://localhost:5000").verify(False))teststeps = [Step(RunTestCase("setUp getUserName").call(RequestWithGetUserName) # 导入后就可以调用了.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()
8.断言
yaml
validate:- eq: [status_code,200]# 断言
- config:name: 测试百度网站base_url: https://www.baidu.com- test:name: 接口名称 百度接口request:url: /method: GETvalidate:- eq: [status_code,200] # 判断相等的4种写法 [实际结果,预期结果]- is: [status_code,200]- ==: [status_code,200]- equals: [status_code,200]- eq: ["${函数名()}", "结果"] # 函数需要引号
eq、equals、==、is,判断实际结果和期望结果是否相等
lt、less_than,判断实际结果小于期望结果
le、less_than_or_equals,判断实际结果小于等于期望结果
gt、greater_than,判断实际结果大于期望结果
ge、greater_than_or_equals,判断实际结果大于等于期望结果
ne、not_equals, 判断实际结果和期望结果不相等
str_eq、string_equals 判断转字符串后对比实际结果和期望结果是否相等
len_eq、length_equals、count_eq 判断字符串或list长度
len_gt、length_greater_than、count_gt、count_greater_than 判断实际结果的长度大于和期望结果
len_ge、length_greater_than_or_equals、count_ge、count_greater_than_or_equals实际结果的长度大于等于期望结果
len_lt、length_less_than、count_lt、count_less_than实际结果的长度小于期望结果
len_le、length_less_than_or_equals、count_le count_less_than_or_equals实际结果的长度小于等于期望结果
9.环境变量
存放在.env文件中,格式为 变量名 = 变量值
${变量名}调用
10.辅助函数debufralk.py
在执行文件中引入该文件函数
import randomdef get_value():return "猫咪"def get_search_word():work = [1, 2, 3, 4]num = random.randint(0, len(work)-1)return num
# 调用 debugtalk.py文件中的函数
- config:name: 百度主页base_url: https://www.baidu.comexport:- title- test:name: 百度搜索request:url: /smethod: GETheaders:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36params:wd: ${get_value()} # 引用函数extract:- title: <title>(.+?)</title>validate:- eq: [$title,"猫_百度搜索"]
11.hook机制,初始化和清理
debugtalk.py
def setup_case():print("测试执行")def teardown_case():print("测试结束")
- config:name: 百度主页base_url: https://www.baidu.comoutput:- title# 放到用例层级setup_hooks:- ${setup_case()}teardown_hooks:- ${teardown_case()}
12.忽略跳过测试用例
-
skip: 无条件跳过
-
skipif: 条件成立跳过
-
skipUnless: 条件不成立跳过
# skip是用来忽略跳过测试用例 - config:name: 百度主页base_url: https://www.baidu.comoutput:- title- test:name: 百度搜索# 忽略跳过用例只能在测试步骤中使用skip: 无条件跳过 # skipIf: True # 条件为 True 时跳过 # skipUnless: False # 条件为 False 时跳过request:url: /smethod: GETheaders:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36params:wd: 猫extract:- title: <title>(.+?)</title>validate:- eq: [$title,"猫_百度搜索"]
13.测试分层
api层
-
对接口进行独立管理
get_access_token.yml
name: 123 base_url: http:... request:url:method: params:name: 13 validate: - eq: [status_code, 2000]
testcase(测试用例)
-
测试用例管理
-
demo1_test.yml
- config:name: 测试export:- token- test:name: 测试用例名称api: ../get_access_token.yml # 引用接口validate: - eq: [content.expires_in, 1000]extract:token: content.access_token
引用同层级用例
-
- config:name: 测试export:- token-test:name: 引用同层级接口testcase: ../demo1_test.ymlextract:token # 引入接口变量- test:name: 测试用例名称api: ../get_access_token.yml # 引用接口validate: - eq: [content.expires_in, 1000]extract:token: content.access_token
接口用例管理(接口套件)
-
config: name: test_suitetestcases:- name: testcase1testcase: ../testcase1.yml- name: testcase2testcase: ../testcase2.yml# 写法2 config: - name: test_suitetestcases:testcase1: # 测试用例名称testcase: ../testcase1.ymltestcase2: testcase: ../testcase2.yml
14.中文乱码
-
添加请求头信息:详情见6.yaml正则提取
-
debugtalk.py解码
# encode编码 decode解码 # iso8859-1 编码,解码成 utf-8 def iso8859_to_utf8(str):return str.encode("iso8859-1").decode("utf-8")# utf-8 编码,解码成 iso8859-1 def utf8_to_iso8859(str):return str.encode("utf-8").decode("iso8859-1")# unicode_escape 编码,解码成 utf-8 def unicode_escape_to_utf8(str):return str.encode("unicode_escape").decode("utf-8")
15.参数化传递(多组数据)
yaml
variables关键字
套件层传给用例层再传给api层
test_suite.yml
config: name: test_suitetestcases:testcase1:testcase: ../testcase1.ymlvariables:search_word: 猫猫 # 参数名: 值
test_case.yml
- config:name: 测试export:- token- test:name: 测试用例名称api: ../get_access_token.yml # 引用接口variables:work: $search_workvalidate: - eq: [content.expires_in, 1000]extract:token: content.access_token
test_api.yml
name: 123
base_url: http:...
request:url:method: wd: $workparams:name: 13
validate: - eq: [status_code, 2000]
parameters关键字
接受多个参数,依次执行
config: name: test_suitetestcases:testcase1:testcase: ../testcase1.ymlparameters:search_word: ["猫猫", "狗狗", "兔兔"] # 参数名: 值
- config:name: 测试export:- token- test:name: 测试用例名称api: ../get_access_token.yml # 引用接口cariables:work: $search_workvalidate: - eq: [content.expires_in, 1000]- eq: ["猫", $result] # $result调用套件层变量的值extract:token: content.access_token
name: 123
base_url: http:...
request:url:method: wd: $workparams:name: 13
validate: - eq: [status_code, 2000]
dubugtalk.py自定义函数
def search_key():return [["猫","猫_百度搜索"],["狗","狗_百度搜索"],["大象","大象_百度搜索"]]
config: name: test_suitetestcases:testcase1:testcase: ../testcase1.ymlparameters:search_word: ${search_key()}
用例层和接口层不变
- config:name: 测试export:- token- test:name: 测试用例名称api: ../get_access_token.yml # 引用接口cariables:work: $search_workvalidate: - eq: [content.expires_in, 1000]- eq: [123, $result] # result是内部变量extract:token: content.access_token
csv参数化
search, result
1, 2
3, 4
config: name: test_suitetestcases:testcase1:testcase: ../testcase1.ymlparameters:search_word: ${P(文件路径)}