HttpTesting 是 HTTP(S) 协议接口测试框架,通过 YAML 来编写测试用例,通过命令行运行代码,不固定目录结构,支持通过命令行生成脚手架。
httptesting 通过 YAML 编写测试用例,安装 httptesting 后通过 amt 命令执行测试用例,支持指定 YAML 中 CASE 名称进行单用例执行,支持指定请求头默认值来共享请求头,支持自定义扩展功能 (在 case 执行根目录下创建 extfunc.py 文件来自定义代码)。
支持多进程执行用例,支持用例执行出错重试功能,支持设定执行用例次数;支持设置控制台输出和报告输出;支持参数化功能与用户自定义用户变量。
https://pypi.org/project/httptesting/#files
https://github.com/HttpTesting/pyhttp
序号 | 版本号 | 描述 |
---|---|---|
1 | v1.0 | 使用 unittest 框架 |
2 | v1.1 | 使用 pytest 框架 |
安装虚拟环境: pip install virtualenv
创建虚拟环境: virtualenv demo_env
命令行模式切换到虚拟环境 Script 目录: /../scripts/
激活虚拟环境: activate.bat
以下三种方式选择其一即可。
已安装 httptesting 包,通过 pip 命令进行更新
pip list 查看 HttpTesting 安装包版本信息
pip install --upgrade HttpTesting
pip install --upgrade HttpTesting==1.0.26
以下四个命令作用相同
序号 | 命令参数 | 描述 |
---|---|---|
1 | am -conf set 或--config set | 此命令用来设置 config.yaml 基本配置 |
2 | am -f template.yaml 或--file template.yaml | 执行 YAML 用例,支持绝对或相对路径 |
3 | am -d testcase 或--dir testcase | 批量执行 testcase 目录下的 YAML 用例,支持绝对路径或相对路径 |
4 | am -sp demo 或--startproject demo | 生成脚手架 demo 目录,以及用例模版 |
5 | am -har httphar.har | 根据抓包工具导出的 http har 文件,生成测试用例 YAML |
6 | am -c demo.yaml 或--convert demo.yaml | 转换数据为 HttpTesting 测试用例 |
[通过开关启用功能:并发执行, 失败重新执行, 用例执行次数, Debug 模式,输出模式 (html 与控制台),URL 基本路径]
URL 设置
钉钉机器人设置
测试报告设置
EMAIL 邮箱设置
用例执行配置
YAML 执行:
[整个 YAML 文件执行,指定 CASE 名称执行,批定多个 CASE 名称执行并且按指定顺序执行]
am -f template.yaml
am -f template.yaml Case1
am -f template.yaml Case2 Case1
YAML 批量执行:
[批量执行 testcase 目录下所有 YAML 测试用例文件]
am -dir testcase
am -sp demo 此命令生成一个 demo 文件夹结构。
脚手架功能,是生成一个测试用例结构与 Case 模版.
执行命令: am -har httphar.har 自动生成 httptesting 用例 har_testcase.yaml。
har 命令来解析, Charles 抓包工具导出的 http .har 请求文件, 自动生成 HttpTesting 用例格式.
TESTCASE{
'case1':['description',{},{}], # 场景模式每个{}一个接口
'case2':['description',{}], # 单接口模式
}
TESTCASE:
#Case1由两个请求组成的场景
Case1:
-
Desc: xxxx业务场景(登录->编辑)
-
Desc: 登录接口
Url: /login/login
Method: GET
Headers:
content-type: "application/json"
cache-control: "no-cache"
Data:
name: "test"
pass: "test123"
OutPara:
"H_token": result.data
"content_type": header.content-type
"name": Data.name
"pass": Data.pass
Assert:
- eq: [result.status, 'success']
-
Desc: 编辑接口
Url: /user/edit
Method: GET
Headers:
content-type: "${content_type}$"
cache-control: "no-cache"
token: "$H_token$"
Data:
name: "${name}$"
pass: "${pass}$"
OutPara:
"$H_token$": result.data
Assert:
- ai: ['success', result.status]
- eq: ['result.status', '修改成功']
TESTCASE:
#同一接口,不同参数,扩充为多个CASE
Case1:
-
Desc: 登录接口-正常登录功能
-
Desc: 登录接口
Url: /login/login
Method: GET
Headers:
content-type: "application/json"
cache-control: "no-cache"
Data:
name: "test"
pass: "test123"
OutPara:
"H_cookie": cookie.SESSION
Assert:
- eq: [result.status, 'success']
Case2:
-
Desc: 登录接口-错误密码
-
Desc: 登录接口
Url: /login/login
Method: GET
Headers:
content-type: "application/json"
cache-control: "no-cache"
Data:
name: "test"
pass: "test123"
OutPara:
"H_cookie": cookie.SESSION
Assert:
- eq: [result.status, 'error']
变量作用域为当前 CASE.
TESTCASE:
Case1:
-
Desc: 接口详细描述
USER_VAR:
token: xxxxxxxx
-
Url: /xxxx/xxxx
Method: POST
Headers:
token: ${token}$
Data:
OutPara:
Assert: []
TEST_CASE:
Case1:
-
Desc: 扫码校验券(支持检测微信券二维码码、微信会员h5券二维码、条码)
USER_VAR:
version: 1.0
data:
req:
sid: '1380598237'
wxcode: "164073966187485312752286" #209736174428
appid: dp0Rm4wNl6A7q6w1QzcZQstr
sig: 9c8c96b38d759abe6633c124a5d37225
v: "${version}$"
ts: 1564643536
- Desc: 扫码校验券
Url: /pos/checkcoupon
Method: POST
Headers:
content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
cache-control: no-cache
Data: ${data}$
OutPara:
Assert:
- eq: [result.errcode, 0]
以上通过 USER_VAR 字典对象来定义变量, key 为变量名, value 为变量值; 使用方法: ${token}$
无需定义变量, USER_VAR 字段在用例中,可以省略.
OutPara 字段用来做公共变量,供其它接口使用,默认为"";
Assert 字段默认为 [].
序号 | 断言方法 | 断言描述 |
---|---|---|
1 | eq: [a, b] | 判断 a 与 b 相等,否则 fail |
2 | nq: [a, b] | 判断 a 与 b 不相等,否则 fail |
3 | al: [a, b] | 判断 a is b 相当于 id(a) == id(b),否则 fail |
4 | at: [a, b] | 判断 a is not b 相当于 id(a) != id(b) |
5 | ai: [a, b] | 判断 a in b ,否则 fail |
6 | ani: [a, b] | 判断 a in not b,否则 fail |
7 | ais: [a, b] | 判断 isinstance(a, b) True |
8 | anis: [a, b] | 判断 isinstance(a, b) False |
9 | ln: [a] | 判断 a is None,否则 fail |
10 | lnn: [a] | 判断 a is not none |
11 | bt: [a] | 判断 a 为 True |
12 | bf: [a] | 判断 a 为 False |
使用原型 (带参数与不带参数)
函数名 | 参数 | 说明 |
---|---|---|
md5 | txt 字符串 | 生成 md5 字符串示例: cbfbf4ea6d7c8032584dcf0defa10276 |
timestamp | - | 秒级时间戳示例: 1563183829 |
uuid1 | - | 生成唯一 id,uuid1 示例:ebcd6df8a77611e99bb588b111064583 |
datetimestr | - | 生成日期时间串,示例:2019-07-16 10:50:16 |
mstimestamp | - | 毫秒级时间戳,20 位 |
sleep_time | - | 线程睡眠,0.5 为 500 毫秒,1 为 1 秒 |
rnd_list | [] | 随机从列表中选择值 |
class Extend:
@staticmethod
def func1():
return 'ext func'
@staticmethod
def func2(args):
return args
定义参数化参数后,同一用例会按照参数个数决定用例执行次数。
TEST_CASE:
Case2:
- Desc: 给指定用户发送验证码
USER_VAR:
cno_list:
- '1674921314241197'
- '1581199496593872'
- '1623770534820512'
- '1674921701066628'
- '1581199096195979'
- '1623770606653991'
PARAM_VAR:
sig: ["1", "2", "3", "4"]
rem: ["4", "5", "6", "7"]
- Desc: 给指定用户发送验证码
Url: /user/sendcode
Method: POST
Headers:
content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
cache-control: no-cache
Data:
req:
cno: '%{rnd_list("${cno_list}$")}%'
appid: dp1svA1gkNt8cQMkoIv7HmD1
sig: "${sig}$"
v: 2.0
ts: 123
OutPara: null
Assert:
- eq:
- result.errcode
- 0
- eq:
- result.res.result
- SUCCESS
当用例中指定全路径时,自去取该路径,当不是绝对路径时,取 BASE_URL 进行智能拼接。
TEST_CASE:
Case1:
- Desc: 给用户发送验证码业务场景(发送1->发送2)
USER_VAR:
cno_list:
- '1674921314241197'
- '1581199496593872'
- '1623770534820512'
- '1674921701066628'
- '1581199096195979'
- '1623770606653991'
REQ_HEADER:
content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
cache-control: no-cache
- Desc: 给指定用户发送验证码1
Url: /user/sendcode
Method: POST
Data:
req:
cno: '%{rnd_list("${cno_list}$")}%'
appid: dp1svA1gkNt8cQMkoIv7HmD1
sig: "123"
v: 2.0
ts: 123
OutPara: null
Assert:
- eq:
- result.errcode
- 0
- eq:
- result.res.result
- SUCCESS
- Desc: 给指定用户发送验证码2
Url: /user/sendcode
Method: POST
Data:
req:
cno: '%{rnd_list("${cno_list}$")}%'
appid: dp1svA1gkNt8cQMkoIv7HmD1
sig: "123"
v: 2.0
ts: 123
Headers:
content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
cache-control: no-cache
OutPara: null
Assert:
- eq:
- result.errcode
- 0
- eq:
- result.res.result
- SUCCESS
TEST_CASE:
Case1:
-
Desc: 给用户发送验证码业务场景(发送1->发送2)
Order: 10
USER_VAR:
cno_list:
- '1674921314241197'
- '1581199496593872'
- '1623770534820512'
- '1674921701066628'
- '1581199096195979'
- '1623770606653991'
REQ_HEADER:
content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
cache-control: no-cache
- Desc: 给指定用户发送验证码1
Url: /user/sendcode
Method: POST
Data:
req:
cno: '%{rnd_list("${cno_list}$")}%'
appid: dp1svA1gkNt8cQMkoIv7HmD1
sig: "123"
v: 2.0
ts: 123
OutPara: null
Assert:
- eq: [result.errcode, 0]
TEST_CASE:
Case1: #用例1
-
Desc: 当日储值统计/charge/today
USER_VAR:
appkey: '0100ff174e808de80db21152ca7dde31'
PARAM_VAR:
sig: ["1", "2"]
rem: ["4", "5"]
CSV_VAR:
file_path: 'd:/deal.csv'
Order: 20
-
Desc: 当日储值统计
Url: /charge/today
Method: POST
Headers:
content-type: "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"
cache-control: "no-cache"
Data:
req:
begin_time: "${name}$"
end_time: "${age}$"
shop_id: 1512995661
appid: 'aaaaa'
sig: "%{sign({'data': 'data.req', 'appid':'data.appid', 'ts':'data.ts', 'v': 'data.v','appkey':'${appkey}$'})}%"
v: 2.0
ts: 1564967996
OutPara:
Assert:
- eq: [result.errcode, 1006]
TEST_CASE:
Case1: #用例1
-
Desc: 会员标记编辑->获取
USER_VAR:
appkey: '4b6ef4ee839dfb0922c28e97143d371e'
Skip: True
-
Desc: 第三方收银会员标记-编辑接口
Url: /userremark/edit
Method: POST
Headers:
content-type: "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"
cache-control: "no-cache"
Data:
req:
uid: "384345911306964992"
token: "adc97617d8c42cd1c99211cab81a2a80"
remark: "123456"
appid: "dp3wY4YtycajNEz23zZpb5Jl"
sig: "%{sign({'data': 'data.req', 'appid':'data.appid', 'ts':'data.ts', 'v': 'data.v','appkey':'${appkey}$'})}%"
v: 2.0
ts: 1564967996
OutPara:
token: data.req.token
remark: data.req.remark
uid: data.req.uid
Assert:
- eq: [result.errcode, 0]
- eq: [result.res, ""]
Skip: False 或 Skip 字段不存在则不会跳过用例执行
从以下用例中可以看出,第二个接口中多了一个 Skip: True 字段,这样执行时,就会跳过该接口,只执行第一个接口。
skip 支持自定义函数:Skip: "%{skip_func()}%"; 但是返回参数必须是 True 或 'True'才能跳过。
TEST_CASE:
Case1:
-
Desc: 当日储值统计/charge/today
USER_VAR:
appkey: '0100ff174e808de80db21152ca7dde31'
# CSV_VAR:
# file_path: 'd:/deal.csv'
Order: 20
-
Desc: 当日储值统计1
Url: /charge/today
Method: POST
Headers:
content-type: "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"
cache-control: "no-cache"
Data:
req:
begin_time: "aaaa"
end_time: "bbbb"
shop_id: 1512995661
appid: 'aaaaa'
sig: "%{sign({'data': 'data.req', 'appid':'data.appid', 'ts':'data.ts', 'v': 'data.v','appkey':'${appkey}$'})}%"
v: 2.0
ts: 1564967996
OutPara:
Assert:
- eq: [result.errcode, 1006]
-
Desc: 当日储值统计2
Skip: True
Url: /charge/today2
Method: POST
Headers:
content-type: "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"
cache-control: "no-cache"
Data:
req:
begin_time: "aaaa"
end_time: "dddd"
shop_id: 1512995661
appid: 'aaaaa'
sig: "%{sign({'data': 'data.req', 'appid':'data.appid', 'ts':'data.ts', 'v': 'data.v','appkey':'${appkey}$'})}%"
v: 2.0
ts: 1564967996
OutPara:
Assert:
- eq: [result.errcode, 1006]
序号 | 功能 | V1.0 | V1.1 | 配置参数 |
---|---|---|---|---|
1 | 并发执行 | - | √ | ENABLE_EXECUTION:False EXECUTION_NUM: 4 |
2 | 失败重新执行 | √ | √ | ENABLE_RERUN: False RERUN_NUM: 2 |
3 | 重复执行 | - | √ | ENABLE_REPEAT: False REPEAT_NUM: 2 |
4 | 钉钉消息 | √ | √ | ENABLE_DDING: False |
5 | 发送报告邮件 | √ | √ | EMAIL_ENABLE: False |
6 | 控制台输出 | - | √ | ENABLE_EXEC_MODE: False |
7 | 自定义函数扩展 | √ | √ | 用例执行 root 目录增加 extfunc.py |
8 | 自定义变量 | √ | √ | 在用例中用 USER_VAR 字段定义变量,作用于当前 Case |
9 | 用例参数化 | √ | √ | 在用例中用 PARAM_VAR 字段定义参数化变量,作用于当前 Case |
10 | 请求头默认值 | √ | √ | 设置用例请求头默认值,整个 case 共享请求头。 |
11 | 指定 case 执行 | - | √ | 单个 yaml 文件指定 case 执行 |
12 | Case 执行顺序 | - | √ | 通过 Order 字段设置 Case 执行优先级 |
13 | Csv 参数化 | - | √ | 通过外部 csv 文件进行参数化 |
14 | case 跳过 | - | √ | 通过在 case 中增加标记 Skip: True |
15 | 场景中单接口跳过 | - | √ | 通过在 case 中增加标记 Skip: True |