安装:
pip install httprunner==2.5.9
起一个脚手架:
hrun --startproject demo
脚手架框架结构:
运行用例(套件、用例、接口曾都可以运行,一般运行套件层,通过套件层组织用例,用例层组织接口):
hrun api/course_list.yml
报告查看:
接口层编写:
name: 课程列表
base_url: http://127.0.0.1:8000
variables:
token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjkyNjY2Mzk5LCJlbWFpbCI6ImFkbWluQGFkbWluLmNvbSJ9.kRVtMOx7WKT7ufksGzfhYVdOusDPBoidOGprFKf2lNo
course_id: 4
request:
url: /course/viewsets/$course_id
method: GET
headers:
Authorization: 'JWT $token'
validate:
- eq: ["status_code", 200]
字段解释:
- name:用例名称
- base_url:域名
- variables:变量,可通过用例层和套件层传入
- request:请求信息,变量可参考requests模块
- validate:断言
变量取值:
在api文件中设置variables,请求username和password通过$变量名的方式直接取variables中设置的变量名
用例层编写:
config:
name: "获取课程列表详情"
variables:
username: ${ENV(USERNAME)}
password: ${ENV(PASSWORD)}
base_url: "http://127.0.0.1:8000"
teststeps:
-
name: 登录
api: api/login.yml
variables:
username: $username
password: $password
extract:
- token: content.token
validate:
- eq: ["status_code", 200]
-
name: 课程列表
api: api/course_list.yml
variables:
token: $token
extract:
- course_id: content.results.0.id
- course_name: content.results.0.name
-
name: 课程详情
api: api/course_detail.yml
variables:
token: $token
course_id: $course_id
validate:
- eq: ['content.name','$course_name']
字段解释:
config:
- name:用例名称
- variables:配置变量
- base_url:域名
teststeps:用例步骤,每个步骤调用一个api yaml文件
api:指定调用api yaml文件路径
extract:提取变量参数,供后面步骤调用
变量取值:
config 和 testteps中的variables名称一样时 优先取config中variables中参数
config中没有设置,取testseps中参数 testcase中参数会覆盖api文件中参数
套件层编写:
config:
name: "课程套件"
variables:
username: ${ENV(USERNAME)}
password: ${ENV(PASSWORD)}
base_url: "http://127.0.0.1:8000"
testcases:
-
name: 查看课程列表
testcase: testcases/course_testcase.yml
variables:
username: $username
password: $password
-
name: 添加课程
testcase: testcases/add_course_testcase.yml
variables:
username: $username
password: $password
字段解释:
config:与用例层字段意义一样
testcases:测试用例,每个测试用例之间相互独立,一个套间可以有多个测试用例
变量优先级:
变量优先级按以下顺序排列:
存在extract提取变量值的,优先取提取中参数content.token
在无extract时,优先取config变量
在无config变量时,优先取用例中变量
以上均无时,取api变量
初始化和清除操作:
yml文件对应代码
config:
name: "获取课程列表详情"
variables:
username: ${ENV(USERNAME)}
password: ${ENV(PASSWORD)}
base_url: "http://127.0.0.1:8000"
setup_hooks: ['${hook_print(用例开始执行)}']
teardown_hooks: ['${hook_print(用例结束执行)}']
teststeps:
-
name: 登录
api: api/login.yml
variables:
username: admin1
password: $password
setup_hooks: ["${setup_hooks_prepare($request)}"]
extract:
- token: content.token
validate:
- eq: ["status_code", 200]
-
name: 课程列表
api: api/course_list.yml
variables:
token: $token
teardown_hooks: ["${teardown_hooks_res($response)}"]
extract:
- course_id: content.results.0.id
- course_name: content.results.0.name
-
name: 课程详情
api: api/course_detail.yml
variables:
token: $token
course_id: $course_id
validate:
- eq: ['content.name','$course_name']
debugtalk对应代码:
def hook_print(msg):
print(msg)
def setup_hooks_prepare(request):
print('步骤开始执行')
if request['method']=='POST' and 'data' in request:
if request.get('headers') is None:request['headers']={}
request['headers']['Content-Type']='application/json'
request['data']=json.dumps(request['data'])
def teardown_hooks_res(response):
print('步骤结束执行')
if response.resp_obj.status_code==200:
try:
data=response.resp_obj.json()
if 'results' in data:
data['results'][0].pop('create_at')
response.resp_obj._content=json.dumps(data).encode('utf8')
except json.decoder.JSONDecodeError as e:
pass
终端执行输出:
一般调用debugtalk方法:${hook_print(用例开始执行)}
如果是hook调用初始化和清除方法:['${hook_print(用例结束执行)}']
值得一提的是如果接口层和用例层调用相同的方法,传入不同的参数,会执行多次,这个是需要注意的点,可以针对代码内容做一个判断,而且会优先执行接口层初始化,
接口层
用例层:
debugtalk代码及执行输出:
接口层和用例层传入一样参数执行输出,可以看到只执行一层:
断言操作:
我们先来看一下httprunner解析yml文件代码:
def get_uniform_comparator(comparator):
"""convert comparator alias to uniform name"""
if comparator in ["eq", "equals", "==", "is"]:
return "equals"
elif comparator in ["lt", "less_than"]:
return "less_than"
elif comparator in ["le", "less_than_or_equals"]:
return "less_than_or_equals"
elif comparator in ["gt", "greater_than"]:
return "greater_than"
elif comparator in ["ge", "greater_than_or_equals"]:
return "greater_than_or_equals"
elif comparator in ["ne", "not_equals"]:
return "not_equals"
elif comparator in ["str_eq", "string_equals"]:
return "string_equals"
elif comparator in ["len_eq", "length_equals", "count_eq"]:
return "length_equals"
elif comparator in [
"len_gt",
"count_gt",
"length_greater_than",
"count_greater_than",
]:
return "length_greater_than"
elif comparator in [
"len_ge",
"count_ge",
"length_greater_than_or_equals",
"count_greater_than_or_equals",
]:
return "length_greater_than_or_equals"
elif comparator in ["len_lt", "count_lt", "length_less_than", "count_less_than"]:
return "length_less_than"
elif comparator in [
"len_le",
"count_le",
"length_less_than_or_equals",
"count_less_than_or_equals",
]:
return "length_less_than_or_equals"
else:
return comparator
字符串相关判断代码:
def contains(check_value, expect_value):
assert isinstance(check_value, (list, tuple, dict, basestring))
assert expect_value in check_value
def contained_by(check_value, expect_value):
assert isinstance(expect_value, (list, tuple, dict, basestring))
assert check_value in expect_value
def type_match(check_value, expect_value):
def get_type(name):
if isinstance(name, type):
return name
elif isinstance(name, basestring):
try:
return __builtins__[name]
except KeyError:
raise ValueError(name)
else:
raise ValueError(name)
assert isinstance(check_value, get_type(expect_value))
def regex_match(check_value, expect_value):
assert isinstance(expect_value, basestring)
assert isinstance(check_value, basestring)
assert re.match(expect_value, check_value)
def startswith(check_value, expect_value):
assert builtin_str(check_value).startswith(builtin_str(expect_value))
def endswith(check_value, expect_value):
assert builtin_str(check_value).endswith(builtin_str(expect_value))
eq: 判断是否相等
lt: 小于
le:小于等于
gt:大于
ge:大于等于
ne:不等于
contains && contains by : 包含和被包含
regex_match: expect_value写一个正则表达式,如果实际结果能正则表达式匹配成功,需要注意的是re.match和re.search是不一样的,match代表正则表达式必须从实际结果字符串开始的地方匹配,如果需要更复杂的场景,可以把httprunner源码copy到项目目录下,将match修改成search
starswith && endswith: 实际结果以期望结果开头、结尾