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: 1 name: kong - id: 2 name: zhenguo city: - '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: GET headers: 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: POST headers: 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.har from 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请求,也可以调用另一个testcase Step( 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: GET extract: # 提取值存储到变量中 - 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: GET headers: # 如果断言为中文的话,加上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.9 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36 extract: - title: <title>(.+?)</title> # 可以使用正则表达式提取 validate: - eq: [$title,"百度一下,你就知道"]
-
解析响应正文
- test: name: 百度主页 request: url: /cgi-bin/tags/get method: GET params: access_token: 49_lsdk_pQJJ4R5IWdWVcDTQu3bHyVOsHDlAcuA99UtVwsmzrtHhSGJKgSPMi3i3TdOQrGeuzZdB62K1uhcKJQAk6eKjzlBL7HgWvAmw7gfiRTp00QnLdSZzN7ul9f2TMPex-Iz2tCg-ZWsSPLbJTJdABAYIY extract: - id: content.tags.0.id # content为根节点 - name: content.tags.0.name validate: - 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: 获取token request: url: /cgi-bin/token method: GET params: grant_type: client_credential appid: wxf144190 secret: 92a113bd4b5 extract: # 提取变量 - token: content.access_token - time: content.expires_in validate: - eq: [$time,7200] - test: name: 获取用户所有标签 request: url: /cgi-bin/tags/get method: GET params: access_token: $token # 引用上面的token实现关联 extract: - id: content.tags.0.id - name: content.tags.0.name validate: - 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: GET headers: Accept: 'text/html' params: name: '' age: '' validate: # 断言 - eq: ['status_code',200] - eq: [content.expires_in,7200] # content表示接口响应的json extract: token: content.access_token
- config: # 配置信息 name: '测试' # 测试用例名称 base_url: 'url' # ip地址 - test: name: '第一步测试' # 测试步骤名称 testcase: test.yaml extract: - token request: url: '/login' # 路径 method: GET headers: 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, RunTestCase 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", "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: GET
validate:
- 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 random
def 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.com
export:
- title
- test:
name: 百度搜索
request:
url: /s
method: GET
headers:
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.9
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36
params:
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.com
output:
- title
# 放到用例层级
setup_hooks:
- ${setup_case()}
teardown_hooks:
- ${teardown_case()}
12.忽略跳过测试用例
-
skip: 无条件跳过
-
skipif: 条件成立跳过
-
skipUnless: 条件不成立跳过
# skip是用来忽略跳过测试用例 - config: name: 百度主页 base_url: https://www.baidu.com output: - title - test: name: 百度搜索 # 忽略跳过用例只能在测试步骤中使用 skip: 无条件跳过 # skipIf: True # 条件为 True 时跳过 # skipUnless: False # 条件为 False 时跳过 request: url: /s method: GET headers: 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.9 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36 params: 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.yml extract: token # 引入接口变量 - test: name: 测试用例名称 api: ../get_access_token.yml # 引用接口 validate: - eq: [content.expires_in, 1000] extract: token: content.access_token
接口用例管理(接口套件)
-
config: name: test_suite testcases: - name: testcase1 testcase: ../testcase1.yml - name: testcase2 testcase: ../testcase2.yml # 写法2 config: - name: test_suite testcases: testcase1: # 测试用例名称 testcase: ../testcase1.yml testcase2: 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_suite
testcases:
testcase1:
testcase: ../testcase1.yml
variables:
search_word: 猫猫 # 参数名: 值
test_case.yml
- config:
name: 测试
export:
- token
- test:
name: 测试用例名称
api: ../get_access_token.yml # 引用接口
variables:
work: $search_work
validate:
- eq: [content.expires_in, 1000]
extract:
token: content.access_token
test_api.yml
name: 123
base_url: http:...
request:
url:
method:
wd: $work
params:
name: 13
validate:
- eq: [status_code, 2000]
parameters关键字
接受多个参数,依次执行
config:
name: test_suite
testcases:
testcase1:
testcase: ../testcase1.yml
parameters:
search_word: ["猫猫", "狗狗", "兔兔"] # 参数名: 值
- config:
name: 测试
export:
- token
- test:
name: 测试用例名称
api: ../get_access_token.yml # 引用接口
cariables:
work: $search_work
validate:
- eq: [content.expires_in, 1000]
- eq: ["猫", $result] # $result调用套件层变量的值
extract:
token: content.access_token
name: 123
base_url: http:...
request:
url:
method:
wd: $work
params:
name: 13
validate:
- eq: [status_code, 2000]
dubugtalk.py自定义函数
def search_key():
return [["猫","猫_百度搜索"],["狗","狗_百度搜索"],["大象","大象_百度搜索"]]
config:
name: test_suite
testcases:
testcase1:
testcase: ../testcase1.yml
parameters:
search_word: ${search_key()}
用例层和接口层不变
- config:
name: 测试
export:
- token
- test:
name: 测试用例名称
api: ../get_access_token.yml # 引用接口
cariables:
work: $search_work
validate:
- eq: [content.expires_in, 1000]
- eq: [123, $result] # result是内部变量
extract:
token: content.access_token
csv参数化
search, result
1, 2
3, 4
config:
name: test_suite
testcases:
testcase1:
testcase: ../testcase1.yml
parameters:
search_word: ${P(文件路径)}