# ApiMeter
*ApiMeter* is a simple & elegant, yet powerful HTTP(S) API testing framework, base on HttpRunner v2.5.9. Enjoy! ✨ 🚀 ✨
## Document
1. ApiMeter 用户使用文档:[https://zhuifengshen.github.io/APIMeter/](https://zhuifengshen.github.io/APIMeter/)
2. ApiMeter PYPI发布版本:[https://pypi.org/project/apimeter](https://pypi.org/project/apimeter)
## Usage
```python
pip install apimeter # 安装,安装后可用内置命令:apimeter、hrun、apilocust
apimeter /path/to/api # 完整生成报告
apimeter /path/to/api --skip-success # 报告忽略成功用例数
```
## 支持新特性
1. 自定义函数的参数支持引用全局变量
```yaml
- eq:
- ${validate_token_v2(content)}
- true
```
2. 自定义函数的参数支持引用全局变量的链式取值
```yaml
- eq:
- ${validate_token(content.token)}
- true
```
3. 自定义函数的参数支持引用自定义变量链式取值
```yaml
- eq:
- ${validate_token($resp.token)}
- true
```
4. 自定义函数支持列表参数解析
```yaml
sign: ${get_sign_v2([$device_sn, $os_platform, $app_version])}
```
5. 自定义函数支持字典对象参数解析
```
sign: "${get_sign_v3({device_sn: $device_sn, os_platform: $os_platform, app_version: $app_version})}"
```
6. 自定义函数支持复杂嵌套对象参数解析
```yaml
- eq:
- "${check_nested_list_fields_not_empty(content, {list_path: productList, nested_list_field: sku, check_fields: [id, amount, origin_amount, currency, account_number, duration]})}"
- True
```
7. 自定义函数支持链式参数|通配符参数|正则表达式参数解析
```yaml
- eq:
- ${check(content, data.product.purchasePlan.*.sku.*.id, data.product.purchasePlan.*.sku.*.amount, data.product.purchasePlan.*.sku.*.origin_amount, data.product.purchasePlan.*.sku.*.currency, data.product.purchasePlan.*.sku.*.account_number, data.product.purchasePlan.*.sku.*.duration)}
- True
- eq:
- ${check(content, '_url ~= ^https?://[^\s/$.?#].[^\s]*$', 'default_currency =* [USD, CNY]', 'default_sku @= dict', 'sku @= list', 'product @= dict')} # 一次性校验所有字段
- True
```
8. 内置全局变量支持转义
全局变量可以在用例中直接使用、作为函数参数入参,同时支持链式取值,引用时无需添加前缀:$。另外支持全局变量转义功能,使用反斜杠'\'将全局变量名作为字面量字符串使用。
- content / body / text / json
- status_code
- cookies
- elapsed
- headers
- encoding
- ok
- reason
- url
```yaml
# 使用示例
status_code
content
content.person.name.first_name
body
body.token
headers
"headers.content-type"
cookies
elapsed.total_seconds
# 特殊情况:当数据字段与全局变量同名时,支持使用反斜杠'\'转义全局变量,将其作为字面量字符串处理
- eq:
- ${check_data_not_null(content.data.linesCollectList.data,2,lines,\content)}
- True
# 这里 \content 会被解析为字符串 "content",而不是全局变量 content 的值
# 支持转义所有全局变量:\content, \body, \text, \json, \status_code, \headers, \cookies, \encoding, \ok, \reason, \url
```
9. 支持自定义脚本校验方式,支持任意python脚本(基于assert校验理念,异常即失败,符合开发直觉)
```yaml
teststeps:
- name: 示例
request:
url: /api/example
method: GET
script:
- assert status_code == 200
# 使用assert语句,支持变量引用和链式取值
- assert content.success is
# 使用自定义函数,异常即失败,否则为通过
- ${custom_validation_function($token)}
# 使用 YAML 的 | 语法编写多行脚本
- |
if status_code == 200:
assert content.success is True
elif status_code == 400:
assert content.error_code is not None
else:
assert False, f"Unexpected status code: {status_code}"
# 循环校验
- |
for item in content.items:
assert item.get("id") is not None
assert item.get("name") is not None
```
10. HTML测试报告支持内容智能折叠和JSON数据树形展示,提升大数据量场景下测试报告的可读性和查看体验
- 当内容超过10行时自动进行折叠显示
- 支持JSON数据、Python对象数据树形结构展示
- 提供彩色语法高亮和节点级别的展开/折叠交互
- 应用于所有关键数据字段
- Request body(请求体)
- Response body(响应体)
- Request headers(请求头)
- Response headers(响应头)
- Validator expect value(校验器期望值)
- Validator actual value(校验器实际值)
- Script(自定义脚本)
- Output(脚本执行结果)
## Validate核心用法
### 1、校验器支持两种格式
```yaml
- {"check": check_item, "comparator": comparator_name, "expect": expect_value} # 一般格式
- comparator_name: [check_item, expect_value] # 简化格式
```
### 2、支持自定义校验器
对于自定义的校验函数,需要遵循三个规则:
- (1)自定义校验函数需放置到debugtalk.py中;
- (2)参数有两个:第一个为原始数据,第二个为原始数据经过运算后得到的预期结果值;
- (3)在校验函数中通过assert将实际运算结果与预期结果值进行比较;
```yaml
# 用例
- test:
name: get token
request:
url: http://127.0.0.1:5000/api/get-token
method: GET
validate:
- {"check": "status_code", "comparator": "eq", "expect": 200}
- {"check": "status_code", "comparator": "sum_status_code", "expect": 2}
# 自定义校验器
def sum_status_code(status_code, expect_sum):
""" sum status code digits
e.g. 400 => 4, 201 => 3
"""
sum_value = 0
for digit in str(status_code):
sum_value += int(digit)
assert sum_value == expect_sum
```
### 3、支持在校验器中引用变量
在结果校验器validate中,check和expect均可实现实现变量的引用;而引用的变量,可以来自四种类型:
- (1)当前teststep中定义的variables,例如expect_status_code
- (2)当前teststep中提取(extract)的结果变量,例如token
- (3)当前测试用例集testset中,先前teststep中提取(extract)的结果变量
- (4)当前测试用例集testset中,全局配置config中定义的变量
```yaml
- test:
name: get token
request:
url: http://127.0.0.1:5000/api/get_token
method: GET
variables:
- expect_status_code: 200
- token_len: 16
extract:
- token: content.token
validate:
- {"check": "status_code", "comparator": "eq", “expect": "$expect_status_code"}
- {"check": "content.token", "comparator": "len_eq", "expect": "$token_len"}
- {"check": "$token", "comparator": "len_eq", "expect": "$token_len"}
```
基于引用变量的特效,可实现更灵活的自定义函数校验器
```yaml
- test:
name: get token
request:
url: http://127.0.0.1:5000/api/get-token
method: GET
validate:
- {"check": "status_code", "comparator": "eq", "expect": 200}
- {"check": "${sum_status_code(status_code)}", "comparator": "eq", "expect": 2}
# 自定义函数
def sum_status_code(status_code):
""" sum status code digits
e.g. 400 => 4, 201 => 3
"""
sum_value = 0
for digit in str(status_code):
sum_value += int(digit)
return sum_value
```
### 4、支持正则表达式提取结果校验内容
假设接口的响应结果内容为LB123abcRB789,那么要提取出abc部分进行校验:
```yaml
- test:
name: get token
request:
url: http://127.0.0.1:5000/api/get-token
method: GET
validate:
- {"check": "LB123(.*)RB789", "comparator": "eq", "expect": "abc"}
```
### 5、内置校验器
| Comparator | Description | A(check), B(expect) | Examples |
|------------------|--------------------------------|------------------------------|----------------------------------------------|
| eq | value is equal | A == B | 9 eq 9 |
| lt | less than | A < B | 7 lt 8 |
| le | less than or equals | A <= B | 7 le 8, 8 le 8 |
| gt | greater than | A > B | 8 gt 7 |
| ge | greater than or equals | A >= B | 8 ge 7, 8 ge 8 |
| ne | not equals | A != B | 6 ne 9 |
| str_eq | string equals | str(A) == str(B) | 123 str_eq '123' |
| len_eq, count_eq | length or count equals | len(A) == B | 'abc' len_eq 3, [1,2] len_eq 2 |
| len_gt, count_gt | length greater than | len(A) > B | 'abc' len_gt 2, [1,2,3] len_gt 2 |
| len_ge, count_ge | length greater than or equals | len(A) >= B | 'abc' len_ge 3, [1,2,3] len_ge 3 |
| len_lt, count_lt | length less than | len(A) < B | 'abc' len_lt 4, [1,2,3] len_lt 4 |
| len_le, count_le | length less than or equals | len(A) <= B | 'abc' len_le 3, [1,2,3] len_le 3 |
| contains | contains | [1, 2] contains 1 | 'abc' contains 'a', [1,2,3] len_lt 4 |
| contained_by | contained by | A in B | 'a' contained_by 'abc', 1 contained_by [1,2] |
| type_match | A is instance of B | isinstance(A, B) | 123 type_match 'int' |
| regex_match | regex matches | re.match(B, A) | 'abcdef' regex 'a|w+d' |
| startswith | starts with | A.startswith(B) is True | 'abc' startswith 'ab' |
| endswith | ends with | A.endswith(B) is True | 'abc' endswith 'bc' |
## 用例 SKIP 机制
1. 无条件跳过:skip: skip this test unconditionally
2. 自定义函数返回True:skipIf: ${skip_test_in_production_env()}
3. 自定义函数返回False:skipUnless: ${skip_test_in_production_env()}
```yaml
# 支持API层
name: subscriptionList_查询
skip: 用例参数变量待适配
base_url: ${get_config(youcloud,graphql_url)}
variables:
user: ${get_config(youcloud,v0_user)}
pwd: ${get_config(youcloud,v0_pwd)}
sessionId: ${get_login(youcloud,$user,$pwd,youcloud_token)}
request:
method: POST
url: /graphql
headers:
Content-Type: application/json; charset=utf-8
Accept-Language: zh
x-operation-name: subscriptionList
cookies:
sessionId: $sessionId
json:
operationName: subscriptionList
query: query subscriptionList{ subscriptionList { brand { app_id, name } } }
variables: {}
validate:
- eq:
- status_code
- 200
# 支持用例层
config:
name: subscriptionList 查询测试
teststeps:
- name: 执行 subscriptionList 查询
skipIf: ${skip_test_in_production_env()}
api: api/youcloud/query_subscriptionList_api.yml
extract:
- data: content.data
validate:
- eq:
- status_code
- 200
```
## 常见注意事项
```yaml
# 日志输出需要指定绝对路径或相对路径,不能指定单独一个文件名(文件可以未创建)
hrun --log-level debug --log-file ./test.log api/youcloud/query_product_api.yml
# 自定义函数使用了字典参数,需要使用双引号包围,避免YAML解析器会将其误认为是字典定义。例如:
sign: "${get_sign_v3({device_sn: $device_sn, os_platform: $os_platform, app_version: $app_version})}"
# 两种转义方式
1. $ 符号转义
$$
2. 全局变量转义
\global_variable,例如:\content
# 一键打包发布,更多内容参考 scripts
make release-patch MESSAGE="支持自动化打包发布,发布版本v2.8.4" # 自动累积小版本
make quick-release VERSION=2.85 MESSAGE="完善使用说明文档,发布版本v2.8.5" # 跳过单元测试
```
## Development
```python
# 本地开发与运行
poetry install # 拉取代码后安装依赖
poetry run python -m apimeter /path/to/api # 完整生成报告
poetry run python -m apimeter /path/to/api --skip-success # 报告忽略成功用例数据
python -m apimeter -h # 查看使用指南
# 测试运行
python -m unittest discover # 运行所有单元测试
python -m unittest tests/test_context.py # 运行指定测试文件
python -m unittest tests.test_api.TestHttpRunner.test_validate_response_content # 运行单个测试用例
python -m tests.api_server 或 PYTHONPATH=. python tests/api_server.py # 启动测试示例服务器
python -m apimeter tests/demo/demo.yml
python -m apimeter tests/testcases --log-level debug --save-tests # 测试示例,同时设置日志与生成中间处理文件
# 打包编译与发布
git tag v1.0.0 或 git tag -a v1.0.0 -m "发布正式版本 v1.0.0" # 打标签(轻量或附注)
git push v1.0.0 或 git push --tags # 推送标签(单个或所有)
poetry build # 打包
poetry publish # 发布,根据提示输入pypi账号密码
pip install -i https://pypi.Python.org/simple/ apimeter # 指定安装源,因为刚发布其他平台未及时同步
# 文档编译与部署
## 1. 本地构建
pip install mkdocs-material==3.3.0
mkdocs build
mkdocs serve
## 2. Gitlab CI 自动化构建
添加.gitlab-ci.yml配置文件,apimeter仓库设置-部署-Pages/完善功能文档,更新mkdocs.yml配置
## 3. Github Action 自动化构建
添加.github/workflows/docs.yml配置文件,apimeter参考设置-pages-Source选择:Deploy from a branch-分支选择:gh-pages(注意避坑:Source不要选择Github Actions、另外添加disable_nojekyll: false不使用默认Jekyll主题)
# 逐行代码运行时内存分析
poetry shell
pip install memory-profiler
# 1. 导入方式
python -m apimeter ~/Project/ATDD/tmp/demo_api/ --skip-success
# 2. 装饰器方式
python -m memory_profiler apimeter ~/Project/ATDD/tmp/demo_api --skip-success --log-level error
# 3. 命令方式
mprof run apimeter /path/to/api
mprof plot # 生成内存趋势图,安装依赖pip install matplotlib
# 参考链接:https://www.cnblogs.com/rgcLOVEyaya/p/RGC_LOVE_YAYA_603days_1.html
```
## 附录
- HttpRunner: https://github.com/httprunner/
- Requests: http://docs.python-requests.org/en/master/
- unittest: https://docs.python.org/3/library/unittest.html
- Locust: http://locust.io/
- har2case: https://github.com/httprunner/har2case
- HAR: http://httparchive.org/
- Swagger: https://swagger.io/
Raw data
{
"_id": null,
"home_page": "https://github.com/httprunner/httprunner",
"name": "apimeter",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.6",
"maintainer_email": null,
"keywords": "HTTP, api, test, requests, locustio",
"author": "debugtalk",
"author_email": "debugtalk@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/71/94/1a0941196f8e703a489b7b674ef7f44560b8de5ec11ec46e80da13c63513/apimeter-2.12.0.tar.gz",
"platform": null,
"description": "# ApiMeter\n\n*ApiMeter* is a simple & elegant, yet powerful HTTP(S) API testing framework, base on HttpRunner v2.5.9. Enjoy! \u2728 \ud83d\ude80 \u2728\n\n\n## Document\n\n1. ApiMeter \u7528\u6237\u4f7f\u7528\u6587\u6863\uff1a[https://zhuifengshen.github.io/APIMeter/](https://zhuifengshen.github.io/APIMeter/)\n2. ApiMeter PYPI\u53d1\u5e03\u7248\u672c\uff1a[https://pypi.org/project/apimeter](https://pypi.org/project/apimeter)\n\n\n## Usage\n```python\npip install apimeter # \u5b89\u88c5\uff0c\u5b89\u88c5\u540e\u53ef\u7528\u5185\u7f6e\u547d\u4ee4\uff1aapimeter\u3001hrun\u3001apilocust\napimeter /path/to/api # \u5b8c\u6574\u751f\u6210\u62a5\u544a\napimeter /path/to/api --skip-success # \u62a5\u544a\u5ffd\u7565\u6210\u529f\u7528\u4f8b\u6570\n```\n\n\n## \u652f\u6301\u65b0\u7279\u6027\n1. \u81ea\u5b9a\u4e49\u51fd\u6570\u7684\u53c2\u6570\u652f\u6301\u5f15\u7528\u5168\u5c40\u53d8\u91cf\n```yaml\n- eq: \n - ${validate_token_v2(content)}\n - true\n``` \n\n2. \u81ea\u5b9a\u4e49\u51fd\u6570\u7684\u53c2\u6570\u652f\u6301\u5f15\u7528\u5168\u5c40\u53d8\u91cf\u7684\u94fe\u5f0f\u53d6\u503c\n```yaml\n- eq: \n - ${validate_token(content.token)}\n - true\n``` \n\n3. \u81ea\u5b9a\u4e49\u51fd\u6570\u7684\u53c2\u6570\u652f\u6301\u5f15\u7528\u81ea\u5b9a\u4e49\u53d8\u91cf\u94fe\u5f0f\u53d6\u503c\n```yaml\n- eq: \n - ${validate_token($resp.token)}\n - true\n``` \n\n4. \u81ea\u5b9a\u4e49\u51fd\u6570\u652f\u6301\u5217\u8868\u53c2\u6570\u89e3\u6790\n```yaml\nsign: ${get_sign_v2([$device_sn, $os_platform, $app_version])}\n```\n\n5. \u81ea\u5b9a\u4e49\u51fd\u6570\u652f\u6301\u5b57\u5178\u5bf9\u8c61\u53c2\u6570\u89e3\u6790\n```\nsign: \"${get_sign_v3({device_sn: $device_sn, os_platform: $os_platform, app_version: $app_version})}\"\n``` \n\n6. \u81ea\u5b9a\u4e49\u51fd\u6570\u652f\u6301\u590d\u6742\u5d4c\u5957\u5bf9\u8c61\u53c2\u6570\u89e3\u6790\n```yaml\n- eq:\n - \"${check_nested_list_fields_not_empty(content, {list_path: productList, nested_list_field: sku, check_fields: [id, amount, origin_amount, currency, account_number, duration]})}\"\n - True\n``` \n\n7. \u81ea\u5b9a\u4e49\u51fd\u6570\u652f\u6301\u94fe\u5f0f\u53c2\u6570\uff5c\u901a\u914d\u7b26\u53c2\u6570\uff5c\u6b63\u5219\u8868\u8fbe\u5f0f\u53c2\u6570\u89e3\u6790\n```yaml\n- eq:\n - ${check(content, data.product.purchasePlan.*.sku.*.id, data.product.purchasePlan.*.sku.*.amount, data.product.purchasePlan.*.sku.*.origin_amount, data.product.purchasePlan.*.sku.*.currency, data.product.purchasePlan.*.sku.*.account_number, data.product.purchasePlan.*.sku.*.duration)}\n - True\n- eq:\n - ${check(content, '_url ~= ^https?://[^\\s/$.?#].[^\\s]*$', 'default_currency =* [USD, CNY]', 'default_sku @= dict', 'sku @= list', 'product @= dict')} # \u4e00\u6b21\u6027\u6821\u9a8c\u6240\u6709\u5b57\u6bb5\n - True \n```\n\n8. \u5185\u7f6e\u5168\u5c40\u53d8\u91cf\u652f\u6301\u8f6c\u4e49\n\n\u5168\u5c40\u53d8\u91cf\u53ef\u4ee5\u5728\u7528\u4f8b\u4e2d\u76f4\u63a5\u4f7f\u7528\u3001\u4f5c\u4e3a\u51fd\u6570\u53c2\u6570\u5165\u53c2\uff0c\u540c\u65f6\u652f\u6301\u94fe\u5f0f\u53d6\u503c\uff0c\u5f15\u7528\u65f6\u65e0\u9700\u6dfb\u52a0\u524d\u7f00\uff1a$\u3002\u53e6\u5916\u652f\u6301\u5168\u5c40\u53d8\u91cf\u8f6c\u4e49\u529f\u80fd\uff0c\u4f7f\u7528\u53cd\u659c\u6760'\\'\u5c06\u5168\u5c40\u53d8\u91cf\u540d\u4f5c\u4e3a\u5b57\u9762\u91cf\u5b57\u7b26\u4e32\u4f7f\u7528\u3002\n\n - content / body / text / json\n - status_code\n - cookies\n - elapsed\n - headers\n - encoding\n - ok\n - reason\n - url\n```yaml\n# \u4f7f\u7528\u793a\u4f8b\nstatus_code\ncontent\ncontent.person.name.first_name\nbody\nbody.token\nheaders\n\"headers.content-type\"\ncookies\nelapsed.total_seconds\n\n# \u7279\u6b8a\u60c5\u51b5\uff1a\u5f53\u6570\u636e\u5b57\u6bb5\u4e0e\u5168\u5c40\u53d8\u91cf\u540c\u540d\u65f6\uff0c\u652f\u6301\u4f7f\u7528\u53cd\u659c\u6760'\\'\u8f6c\u4e49\u5168\u5c40\u53d8\u91cf\uff0c\u5c06\u5176\u4f5c\u4e3a\u5b57\u9762\u91cf\u5b57\u7b26\u4e32\u5904\u7406\n- eq:\n - ${check_data_not_null(content.data.linesCollectList.data,2,lines,\\content)}\n - True\n# \u8fd9\u91cc \\content \u4f1a\u88ab\u89e3\u6790\u4e3a\u5b57\u7b26\u4e32 \"content\"\uff0c\u800c\u4e0d\u662f\u5168\u5c40\u53d8\u91cf content \u7684\u503c\n# \u652f\u6301\u8f6c\u4e49\u6240\u6709\u5168\u5c40\u53d8\u91cf\uff1a\\content, \\body, \\text, \\json, \\status_code, \\headers, \\cookies, \\encoding, \\ok, \\reason, \\url\n```\n\n9. \u652f\u6301\u81ea\u5b9a\u4e49\u811a\u672c\u6821\u9a8c\u65b9\u5f0f\uff0c\u652f\u6301\u4efb\u610fpython\u811a\u672c\uff08\u57fa\u4e8eassert\u6821\u9a8c\u7406\u5ff5\uff0c\u5f02\u5e38\u5373\u5931\u8d25\uff0c\u7b26\u5408\u5f00\u53d1\u76f4\u89c9\uff09\n```yaml\nteststeps:\n- name: \u793a\u4f8b\n request:\n url: /api/example\n method: GET\n script:\n - assert status_code == 200\n # \u4f7f\u7528assert\u8bed\u53e5\uff0c\u652f\u6301\u53d8\u91cf\u5f15\u7528\u548c\u94fe\u5f0f\u53d6\u503c\n - assert content.success is \n # \u4f7f\u7528\u81ea\u5b9a\u4e49\u51fd\u6570\uff0c\u5f02\u5e38\u5373\u5931\u8d25\uff0c\u5426\u5219\u4e3a\u901a\u8fc7\n - ${custom_validation_function($token)}\n # \u4f7f\u7528 YAML \u7684 | \u8bed\u6cd5\u7f16\u5199\u591a\u884c\u811a\u672c\n - |\n if status_code == 200:\n assert content.success is True\n elif status_code == 400:\n assert content.error_code is not None\n else:\n assert False, f\"Unexpected status code: {status_code}\"\n # \u5faa\u73af\u6821\u9a8c\n - |\n for item in content.items:\n assert item.get(\"id\") is not None\n assert item.get(\"name\") is not None\n```\n\n10. HTML\u6d4b\u8bd5\u62a5\u544a\u652f\u6301\u5185\u5bb9\u667a\u80fd\u6298\u53e0\u548cJSON\u6570\u636e\u6811\u5f62\u5c55\u793a\uff0c\u63d0\u5347\u5927\u6570\u636e\u91cf\u573a\u666f\u4e0b\u6d4b\u8bd5\u62a5\u544a\u7684\u53ef\u8bfb\u6027\u548c\u67e5\u770b\u4f53\u9a8c\n - \u5f53\u5185\u5bb9\u8d85\u8fc710\u884c\u65f6\u81ea\u52a8\u8fdb\u884c\u6298\u53e0\u663e\u793a\n - \u652f\u6301JSON\u6570\u636e\u3001Python\u5bf9\u8c61\u6570\u636e\u6811\u5f62\u7ed3\u6784\u5c55\u793a\n - \u63d0\u4f9b\u5f69\u8272\u8bed\u6cd5\u9ad8\u4eae\u548c\u8282\u70b9\u7ea7\u522b\u7684\u5c55\u5f00/\u6298\u53e0\u4ea4\u4e92\n - \u5e94\u7528\u4e8e\u6240\u6709\u5173\u952e\u6570\u636e\u5b57\u6bb5\n - Request body\uff08\u8bf7\u6c42\u4f53\uff09\n - Response body\uff08\u54cd\u5e94\u4f53\uff09\n - Request headers\uff08\u8bf7\u6c42\u5934\uff09\n - Response headers\uff08\u54cd\u5e94\u5934\uff09\n - Validator expect value\uff08\u6821\u9a8c\u5668\u671f\u671b\u503c\uff09\n - Validator actual value\uff08\u6821\u9a8c\u5668\u5b9e\u9645\u503c\uff09\n - Script\uff08\u81ea\u5b9a\u4e49\u811a\u672c\uff09\n - Output\uff08\u811a\u672c\u6267\u884c\u7ed3\u679c\uff09\n \n\n## Validate\u6838\u5fc3\u7528\u6cd5\n\n### 1\u3001\u6821\u9a8c\u5668\u652f\u6301\u4e24\u79cd\u683c\u5f0f\n```yaml\n- {\"check\": check_item, \"comparator\": comparator_name, \"expect\": expect_value} # \u4e00\u822c\u683c\u5f0f\n- comparator_name: [check_item, expect_value] # \u7b80\u5316\u683c\u5f0f\n```\n\n### 2\u3001\u652f\u6301\u81ea\u5b9a\u4e49\u6821\u9a8c\u5668\n\u5bf9\u4e8e\u81ea\u5b9a\u4e49\u7684\u6821\u9a8c\u51fd\u6570\uff0c\u9700\u8981\u9075\u5faa\u4e09\u4e2a\u89c4\u5219\uff1a\n- (1)\u81ea\u5b9a\u4e49\u6821\u9a8c\u51fd\u6570\u9700\u653e\u7f6e\u5230debugtalk.py\u4e2d;\n- (2)\u53c2\u6570\u6709\u4e24\u4e2a\uff1a\u7b2c\u4e00\u4e2a\u4e3a\u539f\u59cb\u6570\u636e\uff0c\u7b2c\u4e8c\u4e2a\u4e3a\u539f\u59cb\u6570\u636e\u7ecf\u8fc7\u8fd0\u7b97\u540e\u5f97\u5230\u7684\u9884\u671f\u7ed3\u679c\u503c;\n- (3)\u5728\u6821\u9a8c\u51fd\u6570\u4e2d\u901a\u8fc7assert\u5c06\u5b9e\u9645\u8fd0\u7b97\u7ed3\u679c\u4e0e\u9884\u671f\u7ed3\u679c\u503c\u8fdb\u884c\u6bd4\u8f83;\n```yaml\n# \u7528\u4f8b\n- test:\n name: get token\n request:\n url: http://127.0.0.1:5000/api/get-token\n method: GET\n validate:\n - {\"check\": \"status_code\", \"comparator\": \"eq\", \"expect\": 200}\n - {\"check\": \"status_code\", \"comparator\": \"sum_status_code\", \"expect\": 2}\n\n# \u81ea\u5b9a\u4e49\u6821\u9a8c\u5668\ndef sum_status_code(status_code, expect_sum):\n \"\"\" sum status code digits\n e.g. 400 => 4, 201 => 3\n \"\"\"\n sum_value = 0\n for digit in str(status_code):\n sum_value += int(digit)\n assert sum_value == expect_sum\n```\n\n### 3\u3001\u652f\u6301\u5728\u6821\u9a8c\u5668\u4e2d\u5f15\u7528\u53d8\u91cf\n\u5728\u7ed3\u679c\u6821\u9a8c\u5668validate\u4e2d\uff0ccheck\u548cexpect\u5747\u53ef\u5b9e\u73b0\u5b9e\u73b0\u53d8\u91cf\u7684\u5f15\u7528\uff1b\u800c\u5f15\u7528\u7684\u53d8\u91cf\uff0c\u53ef\u4ee5\u6765\u81ea\u56db\u79cd\u7c7b\u578b\uff1a\n- \uff081\uff09\u5f53\u524dteststep\u4e2d\u5b9a\u4e49\u7684variables\uff0c\u4f8b\u5982expect_status_code\n- \uff082\uff09\u5f53\u524dteststep\u4e2d\u63d0\u53d6\uff08extract\uff09\u7684\u7ed3\u679c\u53d8\u91cf\uff0c\u4f8b\u5982token\n- \uff083\uff09\u5f53\u524d\u6d4b\u8bd5\u7528\u4f8b\u96c6testset\u4e2d\uff0c\u5148\u524dteststep\u4e2d\u63d0\u53d6\uff08extract\uff09\u7684\u7ed3\u679c\u53d8\u91cf\n- \uff084\uff09\u5f53\u524d\u6d4b\u8bd5\u7528\u4f8b\u96c6testset\u4e2d\uff0c\u5168\u5c40\u914d\u7f6econfig\u4e2d\u5b9a\u4e49\u7684\u53d8\u91cf\n```yaml\n- test:\n name: get token\n request:\n url: http://127.0.0.1:5000/api/get_token\n method: GET\n variables:\n - expect_status_code: 200\n - token_len: 16\n extract:\n - token: content.token\n validate:\n - {\"check\": \"status_code\", \"comparator\": \"eq\", \u201cexpect\": \"$expect_status_code\"}\n - {\"check\": \"content.token\", \"comparator\": \"len_eq\", \"expect\": \"$token_len\"}\n - {\"check\": \"$token\", \"comparator\": \"len_eq\", \"expect\": \"$token_len\"}\n```\n\u57fa\u4e8e\u5f15\u7528\u53d8\u91cf\u7684\u7279\u6548\uff0c\u53ef\u5b9e\u73b0\u66f4\u7075\u6d3b\u7684\u81ea\u5b9a\u4e49\u51fd\u6570\u6821\u9a8c\u5668\n```yaml\n- test:\n name: get token\n request:\n url: http://127.0.0.1:5000/api/get-token\n method: GET\n validate:\n - {\"check\": \"status_code\", \"comparator\": \"eq\", \"expect\": 200}\n - {\"check\": \"${sum_status_code(status_code)}\", \"comparator\": \"eq\", \"expect\": 2}\n\n# \u81ea\u5b9a\u4e49\u51fd\u6570\ndef sum_status_code(status_code):\n \"\"\" sum status code digits\n e.g. 400 => 4, 201 => 3\n \"\"\"\n sum_value = 0\n for digit in str(status_code):\n sum_value += int(digit)\n return sum_value\n```\n\n### 4\u3001\u652f\u6301\u6b63\u5219\u8868\u8fbe\u5f0f\u63d0\u53d6\u7ed3\u679c\u6821\u9a8c\u5185\u5bb9\n\u5047\u8bbe\u63a5\u53e3\u7684\u54cd\u5e94\u7ed3\u679c\u5185\u5bb9\u4e3aLB123abcRB789\uff0c\u90a3\u4e48\u8981\u63d0\u53d6\u51faabc\u90e8\u5206\u8fdb\u884c\u6821\u9a8c\uff1a\n```yaml\n- test:\n name: get token\n request:\n url: http://127.0.0.1:5000/api/get-token\n method: GET\n validate:\n - {\"check\": \"LB123(.*)RB789\", \"comparator\": \"eq\", \"expect\": \"abc\"}\n```\n\n\n### 5\u3001\u5185\u7f6e\u6821\u9a8c\u5668\n| Comparator | Description | A(check), B(expect) | Examples |\n|------------------|--------------------------------|------------------------------|----------------------------------------------|\n| eq | value is equal | A == B | 9 eq 9 |\n| lt | less than | A < B | 7 lt 8 |\n| le | less than or equals | A <= B | 7 le 8, 8 le 8 |\n| gt | greater than | A > B | 8 gt 7 |\n| ge | greater than or equals | A >= B | 8 ge 7, 8 ge 8 |\n| ne | not equals | A != B | 6 ne 9 |\n| str_eq | string equals | str(A) == str(B) | 123 str_eq '123' |\n| len_eq, count_eq | length or count equals | len(A) == B | 'abc' len_eq 3, [1,2] len_eq 2 |\n| len_gt, count_gt | length greater than | len(A) > B | 'abc' len_gt 2, [1,2,3] len_gt 2 |\n| len_ge, count_ge | length greater than or equals | len(A) >= B | 'abc' len_ge 3, [1,2,3] len_ge 3 |\n| len_lt, count_lt | length less than | len(A) < B | 'abc' len_lt 4, [1,2,3] len_lt 4 |\n| len_le, count_le | length less than or equals | len(A) <= B | 'abc' len_le 3, [1,2,3] len_le 3 |\n| contains | contains | [1, 2] contains 1 | 'abc' contains 'a', [1,2,3] len_lt 4 |\n| contained_by | contained by | A in B | 'a' contained_by 'abc', 1 contained_by [1,2] |\n| type_match | A is instance of B | isinstance(A, B) | 123 type_match 'int' |\n| regex_match | regex matches | re.match(B, A) | 'abcdef' regex 'a|w+d' |\n| startswith | starts with | A.startswith(B) is True | 'abc' startswith 'ab' |\n| endswith | ends with | A.endswith(B) is True | 'abc' endswith 'bc' |\n\n\n## \u7528\u4f8b SKIP \u673a\u5236\n\n1. \u65e0\u6761\u4ef6\u8df3\u8fc7\uff1askip: skip this test unconditionally\n2. \u81ea\u5b9a\u4e49\u51fd\u6570\u8fd4\u56deTrue\uff1askipIf: ${skip_test_in_production_env()}\n3. \u81ea\u5b9a\u4e49\u51fd\u6570\u8fd4\u56deFalse\uff1askipUnless: ${skip_test_in_production_env()}\n\n```yaml\n# \u652f\u6301API\u5c42\nname: subscriptionList_\u67e5\u8be2\nskip: \u7528\u4f8b\u53c2\u6570\u53d8\u91cf\u5f85\u9002\u914d\nbase_url: ${get_config(youcloud,graphql_url)}\nvariables:\n user: ${get_config(youcloud,v0_user)}\n pwd: ${get_config(youcloud,v0_pwd)}\n sessionId: ${get_login(youcloud,$user,$pwd,youcloud_token)}\nrequest:\n method: POST\n url: /graphql\n headers:\n Content-Type: application/json; charset=utf-8\n Accept-Language: zh\n x-operation-name: subscriptionList\n cookies:\n sessionId: $sessionId\n json:\n operationName: subscriptionList\n query: query subscriptionList{ subscriptionList { brand { app_id, name } } }\n variables: {}\nvalidate:\n- eq:\n - status_code\n - 200\n\n\n# \u652f\u6301\u7528\u4f8b\u5c42\nconfig:\n name: subscriptionList \u67e5\u8be2\u6d4b\u8bd5\nteststeps:\n- name: \u6267\u884c subscriptionList \u67e5\u8be2\n skipIf: ${skip_test_in_production_env()}\n api: api/youcloud/query_subscriptionList_api.yml\n extract:\n - data: content.data\n validate:\n - eq:\n - status_code\n - 200\n```\n\n\n## \u5e38\u89c1\u6ce8\u610f\u4e8b\u9879\n```yaml\n# \u65e5\u5fd7\u8f93\u51fa\u9700\u8981\u6307\u5b9a\u7edd\u5bf9\u8def\u5f84\u6216\u76f8\u5bf9\u8def\u5f84\uff0c\u4e0d\u80fd\u6307\u5b9a\u5355\u72ec\u4e00\u4e2a\u6587\u4ef6\u540d\uff08\u6587\u4ef6\u53ef\u4ee5\u672a\u521b\u5efa\uff09\nhrun --log-level debug --log-file ./test.log api/youcloud/query_product_api.yml\n\n# \u81ea\u5b9a\u4e49\u51fd\u6570\u4f7f\u7528\u4e86\u5b57\u5178\u53c2\u6570\uff0c\u9700\u8981\u4f7f\u7528\u53cc\u5f15\u53f7\u5305\u56f4\uff0c\u907f\u514dYAML\u89e3\u6790\u5668\u4f1a\u5c06\u5176\u8bef\u8ba4\u4e3a\u662f\u5b57\u5178\u5b9a\u4e49\u3002\u4f8b\u5982\uff1a\nsign: \"${get_sign_v3({device_sn: $device_sn, os_platform: $os_platform, app_version: $app_version})}\"\n\n# \u4e24\u79cd\u8f6c\u4e49\u65b9\u5f0f\n1. $ \u7b26\u53f7\u8f6c\u4e49\n$$\n2. \u5168\u5c40\u53d8\u91cf\u8f6c\u4e49\n\\global_variable\uff0c\u4f8b\u5982\uff1a\\content\n\n# \u4e00\u952e\u6253\u5305\u53d1\u5e03\uff0c\u66f4\u591a\u5185\u5bb9\u53c2\u8003 scripts\nmake release-patch MESSAGE=\"\u652f\u6301\u81ea\u52a8\u5316\u6253\u5305\u53d1\u5e03\uff0c\u53d1\u5e03\u7248\u672cv2.8.4\" # \u81ea\u52a8\u7d2f\u79ef\u5c0f\u7248\u672c\nmake quick-release VERSION=2.85 MESSAGE=\"\u5b8c\u5584\u4f7f\u7528\u8bf4\u660e\u6587\u6863\uff0c\u53d1\u5e03\u7248\u672cv2.8.5\" # \u8df3\u8fc7\u5355\u5143\u6d4b\u8bd5\n```\n\n\n## Development\n```python\n# \u672c\u5730\u5f00\u53d1\u4e0e\u8fd0\u884c\npoetry install # \u62c9\u53d6\u4ee3\u7801\u540e\u5b89\u88c5\u4f9d\u8d56\npoetry run python -m apimeter /path/to/api # \u5b8c\u6574\u751f\u6210\u62a5\u544a\npoetry run python -m apimeter /path/to/api --skip-success # \u62a5\u544a\u5ffd\u7565\u6210\u529f\u7528\u4f8b\u6570\u636e\npython -m apimeter -h # \u67e5\u770b\u4f7f\u7528\u6307\u5357\n\n\n# \u6d4b\u8bd5\u8fd0\u884c\npython -m unittest discover # \u8fd0\u884c\u6240\u6709\u5355\u5143\u6d4b\u8bd5\npython -m unittest tests/test_context.py # \u8fd0\u884c\u6307\u5b9a\u6d4b\u8bd5\u6587\u4ef6\npython -m unittest tests.test_api.TestHttpRunner.test_validate_response_content # \u8fd0\u884c\u5355\u4e2a\u6d4b\u8bd5\u7528\u4f8b\n\npython -m tests.api_server \u6216 PYTHONPATH=. python tests/api_server.py # \u542f\u52a8\u6d4b\u8bd5\u793a\u4f8b\u670d\u52a1\u5668\npython -m apimeter tests/demo/demo.yml\npython -m apimeter tests/testcases --log-level debug --save-tests # \u6d4b\u8bd5\u793a\u4f8b\uff0c\u540c\u65f6\u8bbe\u7f6e\u65e5\u5fd7\u4e0e\u751f\u6210\u4e2d\u95f4\u5904\u7406\u6587\u4ef6\n\n\n# \u6253\u5305\u7f16\u8bd1\u4e0e\u53d1\u5e03\ngit tag v1.0.0 \u6216 git tag -a v1.0.0 -m \"\u53d1\u5e03\u6b63\u5f0f\u7248\u672c v1.0.0\" # \u6253\u6807\u7b7e\uff08\u8f7b\u91cf\u6216\u9644\u6ce8\uff09\ngit push v1.0.0 \u6216 git push --tags # \u63a8\u9001\u6807\u7b7e(\u5355\u4e2a\u6216\u6240\u6709)\npoetry build # \u6253\u5305\npoetry publish # \u53d1\u5e03\uff0c\u6839\u636e\u63d0\u793a\u8f93\u5165pypi\u8d26\u53f7\u5bc6\u7801\npip install -i https://pypi.Python.org/simple/ apimeter # \u6307\u5b9a\u5b89\u88c5\u6e90\uff0c\u56e0\u4e3a\u521a\u53d1\u5e03\u5176\u4ed6\u5e73\u53f0\u672a\u53ca\u65f6\u540c\u6b65\n\n\n# \u6587\u6863\u7f16\u8bd1\u4e0e\u90e8\u7f72 \n## 1. \u672c\u5730\u6784\u5efa\npip install mkdocs-material==3.3.0\nmkdocs build\nmkdocs serve\n\n## 2. Gitlab CI \u81ea\u52a8\u5316\u6784\u5efa\n\u6dfb\u52a0.gitlab-ci.yml\u914d\u7f6e\u6587\u4ef6\uff0capimeter\u4ed3\u5e93\u8bbe\u7f6e-\u90e8\u7f72-Pages/\u5b8c\u5584\u529f\u80fd\u6587\u6863\uff0c\u66f4\u65b0mkdocs.yml\u914d\u7f6e\n\n## 3. Github Action \u81ea\u52a8\u5316\u6784\u5efa\n\u6dfb\u52a0.github/workflows/docs.yml\u914d\u7f6e\u6587\u4ef6\uff0capimeter\u53c2\u8003\u8bbe\u7f6e-pages-Source\u9009\u62e9\uff1aDeploy from a branch-\u5206\u652f\u9009\u62e9\uff1agh-pages\uff08\u6ce8\u610f\u907f\u5751\uff1aSource\u4e0d\u8981\u9009\u62e9Github Actions\u3001\u53e6\u5916\u6dfb\u52a0disable_nojekyll: false\u4e0d\u4f7f\u7528\u9ed8\u8ba4Jekyll\u4e3b\u9898\uff09\n\n\n# \u9010\u884c\u4ee3\u7801\u8fd0\u884c\u65f6\u5185\u5b58\u5206\u6790\npoetry shell\npip install memory-profiler\n# 1. \u5bfc\u5165\u65b9\u5f0f\npython -m apimeter ~/Project/ATDD/tmp/demo_api/ --skip-success\n# 2. \u88c5\u9970\u5668\u65b9\u5f0f\npython -m memory_profiler apimeter ~/Project/ATDD/tmp/demo_api --skip-success --log-level error\n# 3. \u547d\u4ee4\u65b9\u5f0f\nmprof run apimeter /path/to/api\nmprof plot # \u751f\u6210\u5185\u5b58\u8d8b\u52bf\u56fe\uff0c\u5b89\u88c5\u4f9d\u8d56pip install matplotlib\n# \u53c2\u8003\u94fe\u63a5\uff1ahttps://www.cnblogs.com/rgcLOVEyaya/p/RGC_LOVE_YAYA_603days_1.html\n```\n\n\n## \u9644\u5f55\n- HttpRunner: https://github.com/httprunner/\n- Requests: http://docs.python-requests.org/en/master/\n- unittest: https://docs.python.org/3/library/unittest.html\n- Locust: http://locust.io/\n- har2case: https://github.com/httprunner/har2case\n- HAR: http://httparchive.org/\n- Swagger: https://swagger.io/",
"bugtrack_url": null,
"license": "Apache-2.0",
"summary": "One-stop solution for HTTP(S) testing.",
"version": "2.12.0",
"project_urls": {
"Documentation": "https://zhuifengshen.github.io/APIMeter/",
"Homepage": "https://github.com/httprunner/httprunner",
"Repository": "https://github.com/httprunner/httprunner"
},
"split_keywords": [
"http",
" api",
" test",
" requests",
" locustio"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "3ccd4fff58633a31e9f4b795c3a1cb428348e165c6c9616bfc09c7c06a03652f",
"md5": "af98de6a8c543d6c867fd84c6c333c2d",
"sha256": "e4c4f62c415969626aa9717476a6b51159308b5718a23b6f1404639ff633dda6"
},
"downloads": -1,
"filename": "apimeter-2.12.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "af98de6a8c543d6c867fd84c6c333c2d",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.6",
"size": 91639,
"upload_time": "2025-09-05T14:38:35",
"upload_time_iso_8601": "2025-09-05T14:38:35.488008Z",
"url": "https://files.pythonhosted.org/packages/3c/cd/4fff58633a31e9f4b795c3a1cb428348e165c6c9616bfc09c7c06a03652f/apimeter-2.12.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "71941a0941196f8e703a489b7b674ef7f44560b8de5ec11ec46e80da13c63513",
"md5": "4e0d6d5f7a07d852228e99b7921c569b",
"sha256": "10605ab222ec560046c8aab4ea8e731a6b0467690e0a5883df6edf2401f75392"
},
"downloads": -1,
"filename": "apimeter-2.12.0.tar.gz",
"has_sig": false,
"md5_digest": "4e0d6d5f7a07d852228e99b7921c569b",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.6",
"size": 84774,
"upload_time": "2025-09-05T14:38:37",
"upload_time_iso_8601": "2025-09-05T14:38:37.800934Z",
"url": "https://files.pythonhosted.org/packages/71/94/1a0941196f8e703a489b7b674ef7f44560b8de5ec11ec46e80da13c63513/apimeter-2.12.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-09-05 14:38:37",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "httprunner",
"github_project": "httprunner",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "apimeter"
}