一、HttpRunner简介
HttpRunner是一款面向HTTP(S)协议的通用测试框架,只需编写维护一份YAML/JSON文件,即可实现自动化测试、性能测试、线上监控、持续集成等测试需求。
二、HttpRunner特征 (yml文件和json配置文件)
1)支持以YAML/JSON格式定义测试用例
2)支持响应验证
3)支持初始化清除机制
4)支持套件级别的用例管理
5)支持pytest命令(hrun底层封装,h3新特性)
6)支持allure生成测试报告
7)支持性能测试(底层Locust)
三、HttpRunner版本差异
| 2.x | 3.x | |
|---|---|---|
| 推荐格式 | yml | .py |
| 命令行 | 项目实现 | 复用pytest命令 |
| 报告 | 独立实现 | 复用pytest报告生成 |
| 分层 | api、case、suite | RunTestCase、RunRequest |
| 特点 | 代码和case分离 | 链式调用,简化结构 |
四、HttpRunner原理

五、早期框架
1)requests基于urllib3
2)自动化–requests<–性能–locust
3)locust基于requests

六、安装使用
环境说明:
HttpRunner使用python开发,支持python3.6以上版本和大多数操作系统。
安装:
pip install httprunner
验证:
httprunner -V 或 hrun -V
七、五大核心命令
1)httprunner:主命令,用于所有功能。
2)hrun:别名httprunner run,用于运行YAML/JSON/pytest测试用例。
3)hmake:别名httprunner make,用于将YAML/JSON测试案例转换为pytest文件。
4)har2case:别名httprunner har2case,用于将Har转换为YAML/JSON测试用例。
5)locusts:用于运行性能测试。
主要是hrun命令**
八、使用httprunner
1)方式1:录制生成用例(浏览器要默认系统代理,fiddler才抓得到)
1.fiddler抓包导出har格式文件
2.生成用例
har2case listcode.har
{"log":{"pages":[], "comment":"exported @ 2021/8/25 22:56:24", "entries":[{"time":76, "serverIPAddress":"120.55.190.222", "connection":"ClientPort:32736;EgressPort:32738", "request":{"headersSize":501, "postData":{"text":"", "mimeType":""}, "queryString":[{"name":"action", "value":"list_course"}, {"name":"pagenum", "value":"1"}, {"name":"pagesize", "value":"20"}], "headers":[{"name":"Host", "value":"120.55.190.222:7080"}, {"name":"Connection", "value":"keep-alive"}, {"name":"Accept", "value":"application/json, text/plain, */*"}, {"name":"User-Agent", "value":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"}, {"name":"Referer", "value":"http://120.55.190.222:7080/mgr/ps/mgr/index.html"}, {"name":"Accept-Encoding", "value":"gzip, deflate"}, {"name":"Accept-Language", "value":"zh-CN,zh;q=0.9"}, {"name":"Cookie", "value":"sessionid=ej5157gqs5z1ukombfgaxkbmdi4khx2j"}], "bodySize":0, "url":"http://120.55.190.222:7080/api/mgr/sq_mgr/?action=list_course&pagenum=1&pagesize=20", "cookies":[{"name":"sessionid", "value":"ej5157gqs5z1ukombfgaxkbmdi4khx2j"}], "method":"GET", "httpVersion":"HTTP/1.1"}, "timings":{"blocked":-1, "ssl":0, "receive":0, "wait":56, "dns":0, "send":0, "connect":20}, "response":{"headersSize":172, "bodySize":1697, "statusText":"OK", "redirectURL":"", "status":200, "httpVersion":"HTTP/1.1", "cookies":[], "content":{"compression":0, "text":"{\"retcode\": 0, \"retlist\": [{\"id\": 3645, \"name\": \"111\", \"desc\": \"111\", \"display_idx\": 1}, {\"id\": 3648, \"name\": \"\\u82f1\\u8bed\", \"desc\": \"\\u9ad8\\u4e2d\\u82f1\\u8bed\", \"display_idx\": 1}, {\"id\": 3650, \"name\": \"\\u677e\\u52e4123456\", \"desc\": \"123467\", \"display_idx\": 1}, {\"id\": 3652, \"name\": \"\\u677e\\u52e412\", \"desc\": \"1234455\", \"display_idx\": 1}, {\"id\": 3654, \"name\": \"dge\", \"desc\": \"gegheh\", \"display_idx\": 1}, {\"id\": 3655, \"name\": \"g4h4\", \"desc\": \"y4y\", \"display_idx\": 1}, {\"id\": 3656, \"name\": \"g4444\", \"desc\": \"y1234\", \"display_idx\": 1}, {\"id\": 3657, \"name\": \"g4454\", \"desc\": \"y1234\", \"display_idx\": 1}, {\"id\": 3658, \"name\": \"11\", \"desc\": \"11\", \"display_idx\": 1}, {\"id\": 3659, \"name\": \"1122\", \"desc\": \"1111\", \"display_idx\": 1}, {\"id\": 3660, \"name\": \"112233\", \"desc\": \"1111\", \"display_idx\": 1}, {\"id\": 3661, \"name\": \"112233445566\", \"desc\": \"1111\", \"display_idx\": 1}, {\"id\": 3662, \"name\": \"xiaoyi12345\", \"desc\": \"1111\", \"display_idx\": 1}, {\"id\": 3664, \"name\": \"oyitu\\u513f\\u7ae5\", \"desc\": \"\\u70ed\\u70ed\", \"display_idx\": 1}, {\"id\": 3630, \"name\": \"\\u5316\\u5b66\", \"desc\": \"\\u5316\\u5b66\\u8bfe\\u7a0b\", \"display_idx\": 4}, {\"id\": 3631, \"name\": \"\\u521d\\u4e2d\\u5316\\u5b66\", \"desc\": \"\\u521d\\u4e2d\\u6570\\u5b66\\u8bfe\\u7a0b\", \"display_idx\": 4}, {\"id\": 3633, \"name\": \"\\u521d\\u4e2d\\u7269\\u7406\", \"desc\": \"\\u521d\\u4e2d\\u7269\\u7406\\u8bfe\\u7a0b666888\", \"display_idx\": 4}, {\"id\": 3641, \"name\": \"\\u521d\\u4e2d\\u97f3\\u4e50\\u559c\\u5267\\u4eba\", \"desc\": \"\\u521d\\u4e2d\\u97f3\\u4e50\\u559c\\u5267\\u4eba\\u8bfe\\u7a0b\", \"display_idx\": 4}, {\"id\": 3643, \"name\": \"\\u6570\\u5b66\", \"desc\": \"\\u521d\\u4e2d\\u6570\\u5b66\", \"display_idx\": 4}, {\"id\": 3649, \"name\": \"\\u6570\\u5b6611\", \"desc\": \"\\u521d\\u4e2d\\u6570\\u5b66\", \"display_idx\": 4}], \"total\": 27}", "size":1697, "mimeType":"application/json"}, "headers":[{"name":"Content-Type", "value":"application/json"}, {"name":"X-Frame-Options", "value":"SAMEORIGIN"}, {"name":"Content-Length", "value":"1697"}, {"name":"Vary", "value":"Cookie"}, {"name":"Date", "value":"Wed, 25 Aug 2021 14:54:54 GMT"}, {"name":"Server", "value":"0.0.0.0"}]}, "startedDateTime":"2021-08-25T22:54:54.9723173+08:00", "cache":{}}], "creator":{"name":"Fiddler", "comment":"https://fiddler2.com", "version":"5.0.20204.45441"}, "version":"1.2"}}
# NOTE: Generated By HttpRunner v3.1.6
# FROM: listcourse.json
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseListcourse(HttpRunner):
config = Config("testcase description").verify(False)
teststeps = [
Step(
RunRequest("/api/mgr/sq_mgr/")
.get("http://120.55.190.222:7080/api/mgr/sq_mgr/")
.with_params(**{"action": "list_course", "pagenum": "1", "pagesize": "20"})
.with_headers(
**{
"Host": "120.55.190.222:7080",
"Connection": "keep-alive",
"Accept": "application/json, text/plain, */*",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36",
"Referer": "http://120.55.190.222:7080/mgr/ps/mgr/index.html",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cookie": "sessionid=ej5157gqs5z1ukombfgaxkbmdi4khx2j",
}
)
.with_cookies(**{"sessionid": "ej5157gqs5z1ukombfgaxkbmdi4khx2j"})
.validate()
.assert_equal("status_code", 200)
.assert_equal('headers."Content-Type"', "application/json")
.assert_equal("body.retcode", 0)
.assert_equal("body.total", 27)
),
]
if __name__ == "__main__":
TestCaseListcourse().test_start()
3.执行测试用例
1、hrun运行:
hrun listcode_test.py
2、pytest+allure运行:
- allure下载–bin目录配置到环境变量
- 验证:allure --version
- 下载模块:allure-pytest
- 生成临时文件:
pytest listcourse_test.py --alluredir=reports/tmp
或 hrun listcourse_test.py --alluredir=reports/tmp
或 hrun listcourse.yml --alluredir=reports/tmp
hrun可以运行py、yml和json文件,pytest只能运行py文件 - 生成报告:allure serve reports/tmp
har文件转换成yml文件:
har2case -2y listcourse.har
config:
name: testcase description
variables: {}
verify: false
teststeps:
- name: /api/mgr/sq_mgr/
request:
cookies:
sessionid: ej5157gqs5z1ukombfgaxkbmdi4khx2j
headers:
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Cookie: sessionid=ej5157gqs5z1ukombfgaxkbmdi4khx2j
Host: 120.55.190.222:7080
Referer: http://120.55.190.222:7080/mgr/ps/mgr/index.html
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36
method: GET
params:
action: list_course
pagenum: '1'
pagesize: '20'
url: http://120.55.190.222:7080/api/mgr/sq_mgr/
validate:
- eq:
- status_code
- 200
- eq:
- headers.Content-Type
- application/json
- eq:
- body.retcode
- 0
- eq:
- body.total
- 27
har文件转换成json格式文件:
har2case -2j listcourse.har
{
"config": {
"name": "testcase description",
"variables": {},
"verify": false
},
"teststeps": [
{
"name": "/api/mgr/sq_mgr/",
"request": {
"url": "http://120.55.190.222:7080/api/mgr/sq_mgr/",
"params": {
"action": "list_course",
"pagenum": "1",
"pagesize": "20"
},
"method": "GET",
"cookies": {
"sessionid": "ej5157gqs5z1ukombfgaxkbmdi4khx2j"
},
"headers": {
"Host": "120.55.190.222:7080",
"Connection": "keep-alive",
"Accept": "application/json, text/plain, */*",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36",
"Referer": "http://120.55.190.222:7080/mgr/ps/mgr/index.html",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cookie": "sessionid=ej5157gqs5z1ukombfgaxkbmdi4khx2j"
}
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"headers.Content-Type",
"application/json"
]
},
{
"eq": [
"body.retcode",
0
]
},
{
"eq": [
"body.total",
27
]
}
]
}
]
}
json转yaml:
import yaml
import json
def json_to_yaml(data):
stra = json.dumps(data)
dyaml = yaml.load(stra, Loader=yaml.FullLoader)
stream = open("login.yml", 'w')
yaml.safe_dump(dyaml, stream, default_flow_style=False)
json_to_yaml([{"A": 1, "B": 3, "C": [{"D": 3}, {"d": 4}]}, {"E": {"e": [1, 2, 3]}}])
yaml转json:
import yaml
def yaml_to_json():
with open('login.yml', encoding='utf-8') as file:
data = yaml.load(file)
print(data)
yaml_to_json()
2)方式2:手写用例
1.步骤:
1、熟悉hr测试用例规则
2、根据规则编写yaml/json/python格式的测试用例文件
3、执行测试用例
2.用例的整体结构
1、config(每个测试用例都必须有config部分,可以配置用例)
必填项:
name:测试用例的名称,将在log和报告中展示。
选填项:
base_url ip+port
variables
parameters
verify
export
2、teststeps(包含测试步骤相关信息,其中步骤可以引用其他测试用例)
必填项:
name:测试步骤的名称,将在log和报告中展示。
request:嵌套字段,包含请求信息。
url可以填写相对路径,与base_url拼接
选填项:
extract 提取返回值
validate
hooks
3、yaml书写规范
注意:
key和value之间要加空格
‘-’表示在列表里面
运行:
hrun testcase/demo1.yml
直接运行所有用例:hrun testcase
九、httprunner参数化
1)用例变量类型划分
1.配置变量(variables):
用于数据解耦
variables参数定义在config或teststeps中(优先级高)
2.参数变量(parameters)
用于参数化—列表类型
parameters参数定义在config中
2)按级别划分
1.用例级别
字典形式
方式1:直接指定参数方式
方式2:引用自定义函数–返回字典–创建debugtalk的py文件,定义函数(文件放在第一层目录)
#! /usr/bin/python3
# -*- coding:utf-8 -*-
# Author:ChenBin
# time:2021/8/291:08
# file:debugtalk.py
import requests
def login_variables():
return {"user": "auto", "psw": "sdfsdfsdf"}
def login_parameter():
# 可以从文件获取参数
return [
{"user": "auto", "psw": "sdfsdfsdf", "code": 0},
{"user": "auto1", "psw": "sdfsdfsdf", "code": 1},
{"user": "auto", "psw": "sdfsdf", "code": 1}
]
def hook_setup():
with open('setup.txt', 'w', encoding='utf8') as f:
f.write('执行初始化')
def hook_teardown():
with open('teardown.txt', 'w', encoding='utf8') as f:
f.write('执行清除')
url = 'http://120.55.190.222:7080'
cookie = {}
def login_course():
global sessionid
if 'sessionid' not in cookie:
payload = {"username": "auto", "password": "sdfsdfsdf"}
res = requests.post(url + '/api/mgr/loginReq', data=payload)
sessionid = res.cookies.get_dict()['sessionid']
cookie['sessionid'] = sessionid
else:
sessionid = cookie['sessionid']
return sessionid
def add_course():
sessionid = login_course()
payload = {"action": "add_course",
"data": '{"name": "测试课程", "desc": "初中测试课程", "display_idx": "1"}'
}
cookies = {'sessionid': sessionid}
resp = requests.post(url + '/api/mgr/sq_mgr/', data=payload, cookies=cookies)
return resp.json()['id']
def delete_course():
course_id = add_course()
payload = {"action": "delete_course", "id": course_id}
resp = requests.delete(url + '/api/mgr/sq_mgr/', data=payload, cookies=cookie)
return resp.json()
print(delete_course())
login.yml
config: # 用例配置区
name: 列出课程测试
base_url: http://120.55.190.222:7080
verify: false # 非https模式
export: # 返回测试步骤中提取的变量--数据类型--列表
-
cookie
teststeps: # 测试步骤--对应数据类型:列表
-
name: 登录
request:
method: POST
url: /api/mgr/loginReq
data:
username: auto
password: sdfsdfsdf
extract: # 对应数据格式--字典
cookie: cookies.sessionid
length: headers.Content-Length # jmespath
add_course.yml
config: # 用例配置区
name: 添加课程
base_url: http://120.55.190.222:7080
verify: false # 非https模式
export:
- course_id
teststeps:
-
name: 登录
testcase: testcase/login.yml
-
name: 添加课程
request:
method: POST
url: /api/mgr/sq_mgr/
data:
action: add_course
data: '{"name": "初中化学", "desc": "初中化学课程", "display_idx": "1"}'
validate:
-
eq: ['status_code', 200]
-
eq: ['body.retcode', 0]
extract:
course_id: body.id
update_course.yml
config: # 用例配置区
name: 修改课程
base_url: http://120.55.190.222:7080
verify: false # 非https模式
export:
- course_id
teststeps:
-
name: 添加课程
testcase: testcase/add_course.yml
-
name: 修改课程
request:
method: PUT
url: /api/mgr/sq_mgr/
data:
action: modify_course
id: ${course_id}
newdata: '{"name": "初中计算机", "desc": "初中计算机课程", "display_idx": "1"}'
validate:
-
eq: ['status_code', 200]
-
eq: ['body.retcode', 0]
delete_course.yml
config: # 用例配置区
name: 删除课程
base_url: http://120.55.190.222:7080
verify: false # 非https模式
teststeps:
-
name: 添加并修改课程
testcase: testcase/update_course.yaml
-
name: 删除课程
request:
method: DELETE
url: /api/mgr/sq_mgr/
data:
action: delete_course
id: $course_id
validate:
-
eq: ['status_code', 200]
-
eq: ['body.retcode', 0]
2.步骤级别
1、直接在teststeps加参数variables,且引用函数不支持
demo_variables_login.yml
config: # 用例配置区
name: 列出课程测试
base_url: http://120.55.190.222:7080
verify: false # 非https模式
# variables: ${login_variables()} # 用例变量--字典类型
# {"user": "auto", "psw": "sdfsdfsdf"}
# user: auto # 方式1.直接指定参数方式
# psw: sdfsdfsdf # 方式2:引用自定义函数--返回字典
variables:
user: ${ENV(username)}
psw: ${ENV(password)}
teststeps: # 测试步骤--对应数据类型:列表
-
name: 登录
# variables:
# user: auto
# psw: sdfsdfsdf
# variables: ${login_variables()} # 不支持???
request:
method: POST
url: /api/mgr/loginReq
data:
username: ${user}
password: ${psw}
extract: # 对应数据格式--字典
cookie: cookies.sessionid
length: headers.Content-Length # jmespath
validate:
- eq: [ 'status_code', 200]
- eq: [ 'body.retcode', 0 ]
update_course.yml
config: # 用例配置区
name: 修改课程
base_url: http://120.55.190.222:7080
verify: false # 非https模式
variables:
course_id: ${add_course('高级课程')}
teststeps:
-
name: 登录
testcase: setup_teardown/login.yml
-
name: 修改课程
setup_hooks:
- ${add_course()}
teardown_hooks:
- ${delete_course()}
request:
method: PUT
url: /api/mgr/sq_mgr/
data:
action: modify_course
id: ${course_id}
newdata: '{"name": "初中计算机", "desc": "初中计算机课程", "display_idx": "1"}'
validate:
-
eq: ['status_code', 200]
-
eq: ['body.retcode', 0]
2、前置和后置
步骤级别:hook函数
setup_hooks,teardown_hooks
login.yml
config: # 用例配置区
name: 列出课程测试
base_url: http://120.55.190.222:7080
verify: false # 非https模式
export: # 返回测试步骤中提取的变量--数据类型--列表
-
cookie
teststeps: # 测试步骤--对应数据类型:列表
-
name: 登录
setup_hooks:
- ${hook_setup()}
teardown_hooks:
- ${hook_teardown()}
request:
method: POST
url: /api/mgr/loginReq
data:
username: auto
password: sdfsdfsdf
extract: # 对应数据格式--字典
cookie: cookies.sessionid
length: headers.Content-Length # jmespath
用例级别:未提供
3)环境变量
创建ENV文件
注意:顶行不要空,直接写变量=变量值
login.env
username=auto
password=sdfsdfsdf
login.yml
config: # 用例配置区
name: 列出课程测试
base_url: http://120.55.190.222:7080
verify: false # 非https模式
# variables: ${login_variables()} # 用例变量--字典类型
# {"user": "auto", "psw": "sdfsdfsdf"}
# user: auto # 方式1.直接指定参数方式
# psw: sdfsdfsdf # 方式2:引用自定义函数--返回字典
variables:
user: ${ENV(username)}
psw: ${ENV(password)}
teststeps: # 测试步骤--对应数据类型:列表
-
name: 登录
# variables:
# user: auto
# psw: sdfsdfsdf
# variables: ${login_variables()} # 不支持???
request:
method: POST
url: /api/mgr/loginReq
data:
username: ${user}
password: ${psw}
extract: # 对应数据格式--字典
cookie: cookies.sessionid
length: headers.Content-Length # jmespath
validate:
- eq: [ 'status_code', 200]
- eq: [ 'body.retcode', 0 ]
十、测试套运行

config配置
独立参数:
运用用例数=独立参数的笛卡尔积
关联参数:
运行用例数=关联参数组
test_suite.yml
config:
name: test suite demo
testcases:
-
name: case1
testcase: testcase/demo1.yml # 定义测试用例文件路径
-
name: case2
testcase: testcase/login.yml
login.yml
config: # 用例配置区
name: 列出课程测试
base_url: http://120.55.190.222:7080
verify: false # 非https模式
export: # 返回测试步骤中提取的变量--数据类型--列表
-
cookie
teststeps: # 测试步骤--对应数据类型:列表
-
name: 登录
setup_hooks:
- ${hook_setup()}
teardown_hooks:
- ${hook_teardown()}
request:
method: POST
url: /api/mgr/loginReq
data:
username: auto
password: sdfsdfsdf
extract: # 对应数据格式--字典
cookie: cookies.sessionid
length: headers.Content-Length # jmespath
demo1.yml
config: # 用例配置区
name: 列出课程测试
base_url: http://120.55.190.222:7080
verify: false # 非https模式
teststeps: # 测试步骤--对应数据类型:列表
-
name: 登录
testcase: testcase/login.yml
-
name: 步骤2-列出课程
request:
method: GET # 请求方法 GET POST PUT DELETE
url: /api/mgr/sq_mgr/
params:
action: list_course
pagenum: 1
pagesize: 20
cookies:
sessionid: ${cookie} # 引用变量
validate: # 校验 对应列表数据
-
equal: # 判断实际值和预期是否相等,对应数据类型--列表
- status_code
- 200
-
equal:
- body.retcode
- 0
-
equal:
- body.total
- 30
903

被折叠的 条评论
为什么被折叠?



