mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
Merge pull request #872 from httprunner/dev
3.0-dev **Added** - feat: add default header `HRUN-Request-ID` for each testcase #721
This commit is contained in:
@@ -5,14 +5,17 @@
|
||||
**Added**
|
||||
|
||||
- feat: dump log for each testcase
|
||||
- feat: add default header `HRUN-Request-ID` for each testcase #721
|
||||
|
||||
**Changed**
|
||||
|
||||
- replace logging with [loguru](https://github.com/Delgan/loguru)
|
||||
- remove support for Python 2.7
|
||||
- replace logging with [loguru](https://github.com/Delgan/loguru)
|
||||
- replace string format with f-string
|
||||
- remove dependency colorama and colorlog
|
||||
- generate reports/logs folder in current working directory
|
||||
- remove cli `--validate`
|
||||
- remove cli `--pretty`
|
||||
|
||||
## 2.5.7 (2020-02-21)
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
|
||||
HttpRunner 从 `1.3.1` 版本开始,支持对 JSON 格式测试用例的内容进行格式正确性检测和样式美化功能。
|
||||
|
||||
## JSON 格式正确性检测
|
||||
|
||||
若需对 JSON 格式用例文件的内容进行正确性检测,可使用 `--validate` 参数。
|
||||
|
||||
可指定单个 JSON 用例文件路径。
|
||||
|
||||
```bash
|
||||
$ hrun --validate docs/data/demo-quickstart.json
|
||||
Start to validate JSON file: docs/data/demo-quickstart.json
|
||||
OK
|
||||
```
|
||||
|
||||
也可指定多个 JSON 用例文件路径。
|
||||
|
||||
```bash
|
||||
$ hrun --validate docs/data/demo-quickstart.json docs/data/demo-quickstart.yml docs/data/demo-quickstart-0.json
|
||||
Start to validate JSON file: docs/data/demo-quickstart.json
|
||||
OK
|
||||
WARNING Only JSON file format can be validated, skip docs/data/demo-quickstart.yml
|
||||
Start to validate JSON file: docs/data/demo-quickstart-0.json
|
||||
OK
|
||||
```
|
||||
|
||||
如上所示,当传入的文件后缀不是`.json`,HttpRunner 会打印 WARNING 信息,并跳过检测。
|
||||
|
||||
若 JSON 文件格式正确,则打印 OK。
|
||||
|
||||
若 JSON 文件格式存在异常,则打印详细的报错信息,精确到错误在文件中出现的行和列。
|
||||
|
||||
```bash
|
||||
$ hrun --validate docs/data/demo-quickstart.json
|
||||
Start to validate JSON file: docs/data/demo-quickstart.json
|
||||
Expecting ',' delimiter: line 5 column 13 (char 82)
|
||||
```
|
||||
|
||||
## JSON 格式美化
|
||||
|
||||
与 YAML 格式不同,JSON 格式不强制要求缩进和换行,这有点类似于 C 语言和 Python 语言的差异。
|
||||
|
||||
例如,`demo-quickstart.json`文件也可以改写为如下形式。
|
||||
|
||||
```json
|
||||
[{"config": {"name": "testcase description","variables": [],"request": {"base_url": "","headers": {"User-Agent": "python-requests/2.18.4"}}}},{"test": {"name": "/api/get-token","request": {"url": "http://127.0.0.1:5000/api/get-token","headers": {"device_sn": "FwgRiO7CNA50DSU","user_agent": "iOS/10.3","os_platform": "ios","app_version": "2.8.6","Content-Type": "application/json"},"method": "POST","json": {"sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98"}},"validate": [{"eq": ["status_code",200]},{"eq": ["headers.Content-Type","application/json"]},{"eq": ["content.success",true]},{"eq": ["content.token","baNLX1zhFYP11Seb"]}]}},{"test": {"name": "/api/users/1000","request": {"url": "http://127.0.0.1:5000/api/users/1000","headers": {"device_sn": "FwgRiO7CNA50DSU","token": "baNLX1zhFYP11Seb","Content-Type": "application/json"},"method": "POST","json": {"name": "user1","password": "123456"}},"validate": [{"eq": ["status_code",201]},{"eq": ["headers.Content-Type","application/json"]},{"eq": ["content.success",true]},{"eq": ["content.msg","user created successfully."]}]}}]
|
||||
```
|
||||
|
||||
虽然上面 JSON 格式的测试用例也能正常执行,但测试用例文件的可读性太差,不利于阅读和维护。
|
||||
|
||||
针对该需求,可使用 `--prettify` 参数对 JSON 格式用例文件进行样式美化。
|
||||
|
||||
可指定单个 JSON 用例文件路径。
|
||||
|
||||
```bash
|
||||
$ hrun --prettify docs/data/demo-quickstart.json
|
||||
Start to prettify JSON file: docs/data/demo-quickstart.json
|
||||
success: docs/data/demo-quickstart.pretty.json
|
||||
```
|
||||
|
||||
也可指定多个 JSON 用例文件路径。
|
||||
|
||||
```bash
|
||||
$ hrun --prettify docs/data/demo-quickstart.json docs/data/demo-quickstart.yml docs/data/demo-quickstart-0.json
|
||||
WARNING Only JSON file format can be prettified, skip: docs/data/demo-quickstart.yml
|
||||
Start to prettify JSON file: docs/data/demo-quickstart.json
|
||||
success: docs/data/demo-quickstart.pretty.json
|
||||
Start to prettify JSON file: docs/data/demo-quickstart-0.json
|
||||
success: docs/data/demo-quickstart-0.pretty.json
|
||||
```
|
||||
|
||||
如上所示,当传入的文件后缀不是`.json`,HttpRunner 会打印 WARNING 信息,并跳过检测。
|
||||
|
||||
若转换成功,则打印美化后的文件路径;若 JSON 文件格式存在异常,则打印详细的报错信息,精确到错误在文件中出现的行和列。
|
||||
@@ -50,7 +50,7 @@ class HttpRunner(object):
|
||||
self.test_loader = unittest.TestLoader()
|
||||
self.save_tests = save_tests
|
||||
self._summary = None
|
||||
self.project_mapping = None
|
||||
self.test_path = None
|
||||
|
||||
def _add_tests(self, testcases):
|
||||
""" initialize testcase with Runner() and add to test suite.
|
||||
@@ -139,7 +139,7 @@ class HttpRunner(object):
|
||||
log_handler = None
|
||||
if self.save_tests:
|
||||
logs_file_abs_path = utils.prepare_log_file_abs_path(
|
||||
self.project_mapping, f"testcase_{index+1}.log"
|
||||
self.test_path, f"testcase_{index+1}.log"
|
||||
)
|
||||
log_handler = logger.add(logs_file_abs_path, level="DEBUG")
|
||||
|
||||
@@ -197,10 +197,11 @@ class HttpRunner(object):
|
||||
|
||||
if self.save_tests:
|
||||
logs_file_abs_path = utils.prepare_log_file_abs_path(
|
||||
self.project_mapping, f"testcase_{index + 1}.log"
|
||||
self.test_path, f"testcase_{index+1}.log"
|
||||
)
|
||||
testcase_summary["log"] = logs_file_abs_path
|
||||
|
||||
testcase_summary["HRUN-Request-ID"] = testcase.runner.hrun_request_id
|
||||
summary["details"].append(testcase_summary)
|
||||
|
||||
return summary
|
||||
@@ -209,10 +210,13 @@ class HttpRunner(object):
|
||||
""" run testcase/testsuite data
|
||||
"""
|
||||
capture_message("start to run tests")
|
||||
self.project_mapping = tests_mapping.get("project_mapping", {})
|
||||
self.test_path = tests_mapping.get("project_mapping", {}).get("test_path", "")
|
||||
|
||||
if self.save_tests:
|
||||
utils.dump_logs(tests_mapping, self.project_mapping, "loaded")
|
||||
utils.dump_json_file(
|
||||
tests_mapping,
|
||||
utils.prepare_log_file_abs_path(self.test_path, "loaded.json")
|
||||
)
|
||||
|
||||
# parse tests
|
||||
self.exception_stage = "parse tests"
|
||||
@@ -220,14 +224,20 @@ class HttpRunner(object):
|
||||
parse_failed_testfiles = parser.get_parse_failed_testfiles()
|
||||
if parse_failed_testfiles:
|
||||
logger.warning("parse failures occurred ...")
|
||||
utils.dump_logs(parse_failed_testfiles, self.project_mapping, "parse_failed")
|
||||
utils.dump_json_file(
|
||||
parse_failed_testfiles,
|
||||
utils.prepare_log_file_abs_path(self.test_path, "parse_failed.json")
|
||||
)
|
||||
|
||||
if len(parsed_testcases) == 0:
|
||||
logger.error("failed to parse all cases, abort.")
|
||||
raise exceptions.ParseTestsFailure
|
||||
|
||||
if self.save_tests:
|
||||
utils.dump_logs(parsed_testcases, self.project_mapping, "parsed")
|
||||
utils.dump_json_file(
|
||||
parsed_testcases,
|
||||
utils.prepare_log_file_abs_path(self.test_path, "parsed.json")
|
||||
)
|
||||
|
||||
# add tests to test suite
|
||||
self.exception_stage = "add tests to test suite"
|
||||
@@ -246,10 +256,16 @@ class HttpRunner(object):
|
||||
report.stringify_summary(self._summary)
|
||||
|
||||
if self.save_tests:
|
||||
utils.dump_logs(self._summary, self.project_mapping, "summary")
|
||||
utils.dump_json_file(
|
||||
self._summary,
|
||||
utils.prepare_log_file_abs_path(self.test_path, "summary.json")
|
||||
)
|
||||
# save variables and export data
|
||||
vars_out = self.get_vars_out()
|
||||
utils.dump_logs(vars_out, self.project_mapping, "io")
|
||||
utils.dump_json_file(
|
||||
vars_out,
|
||||
utils.prepare_log_file_abs_path(self.test_path, "io.json")
|
||||
)
|
||||
|
||||
return self._summary
|
||||
|
||||
|
||||
@@ -5,12 +5,10 @@ import sys
|
||||
import sentry_sdk
|
||||
from loguru import logger
|
||||
|
||||
from httprunner import __description__, __version__, exceptions
|
||||
from httprunner import __description__, __version__
|
||||
from httprunner.api import HttpRunner
|
||||
from httprunner.loader import load_cases
|
||||
from httprunner.report import gen_html_report
|
||||
from httprunner.utils import (create_scaffold,
|
||||
prettify_json_file, init_sentry_sdk)
|
||||
from httprunner.utils import create_scaffold, init_sentry_sdk
|
||||
|
||||
init_sentry_sdk()
|
||||
|
||||
@@ -52,12 +50,6 @@ def main():
|
||||
parser.add_argument(
|
||||
'--startproject',
|
||||
help="Specify new project name.")
|
||||
parser.add_argument(
|
||||
'--validate', nargs='*',
|
||||
help="Validate YAML/JSON api/testcase/testsuite format.")
|
||||
parser.add_argument(
|
||||
'--prettify', nargs='*',
|
||||
help="Prettify JSON testcase format.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -70,22 +62,6 @@ def main():
|
||||
print(f"{__version__}")
|
||||
sys.exit(0)
|
||||
|
||||
if args.validate:
|
||||
for validate_path in args.validate:
|
||||
try:
|
||||
logger.info(f"validate test file: {validate_path}")
|
||||
load_cases(validate_path, args.dot_env_path)
|
||||
except exceptions.MyBaseError as ex:
|
||||
logger.error(str(ex))
|
||||
continue
|
||||
|
||||
logger.info("done!")
|
||||
sys.exit(0)
|
||||
|
||||
if args.prettify:
|
||||
prettify_json_file(args.prettify)
|
||||
sys.exit(0)
|
||||
|
||||
project_name = args.startproject
|
||||
if project_name:
|
||||
create_scaffold(project_name)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# encoding: utf-8
|
||||
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from unittest.case import SkipTest
|
||||
|
||||
@@ -73,6 +72,12 @@ class Runner(object):
|
||||
self.export = config.get("export") or config.get("output", [])
|
||||
config_variables = config.get("variables", {})
|
||||
|
||||
self.hrun_request_id = str(uuid.uuid4())
|
||||
if "HRUN-Request-ID" not in config_variables:
|
||||
config_variables["HRUN-Request-ID"] = self.hrun_request_id
|
||||
else:
|
||||
self.hrun_request_id = config_variables["HRUN-Request-ID"]
|
||||
|
||||
# testcase setup hooks
|
||||
testcase_setup_hooks = config.get("setup_hooks", [])
|
||||
# testcase teardown hooks
|
||||
@@ -222,6 +227,11 @@ class Runner(object):
|
||||
base_url = self.session_context.eval_content(test_dict.get("base_url", ""))
|
||||
parsed_url = utils.build_url(base_url, url)
|
||||
|
||||
request_headers = parsed_test_request.setdefault("headers", {})
|
||||
if "HRUN-Request-ID" not in request_headers:
|
||||
parsed_test_request["headers"]["HRUN-Request-ID"] = \
|
||||
self.session_context.session_variables_mapping["HRUN-Request-ID"]
|
||||
|
||||
try:
|
||||
method = parsed_test_request.pop('method')
|
||||
parsed_test_request.setdefault("verify", self.verify)
|
||||
|
||||
@@ -8,6 +8,7 @@ import json
|
||||
import os.path
|
||||
import re
|
||||
import uuid
|
||||
from typing import Union
|
||||
|
||||
import sentry_sdk
|
||||
from loguru import logger
|
||||
@@ -514,33 +515,6 @@ def gen_cartesian_product(*args):
|
||||
return product_list
|
||||
|
||||
|
||||
def prettify_json_file(file_list):
|
||||
""" prettify JSON testcase format
|
||||
"""
|
||||
for json_file in set(file_list):
|
||||
if not json_file.endswith(".json"):
|
||||
logger.warning(f"Only JSON file format can be prettified, skip: {json_file}")
|
||||
continue
|
||||
|
||||
logger.info(f"Start to prettify JSON file: {json_file}")
|
||||
|
||||
dir_path = os.path.dirname(json_file)
|
||||
file_name, file_suffix = os.path.splitext(os.path.basename(json_file))
|
||||
outfile = os.path.join(dir_path, f"{file_name}.pretty.json")
|
||||
|
||||
with io.open(json_file, 'r', encoding='utf-8') as stream:
|
||||
try:
|
||||
obj = json.load(stream)
|
||||
except ValueError as e:
|
||||
raise SystemExit(e)
|
||||
|
||||
with io.open(outfile, 'w', encoding='utf-8') as out:
|
||||
json.dump(obj, out, indent=4, separators=(',', ': '))
|
||||
out.write('\n')
|
||||
|
||||
print(f"success: {outfile}")
|
||||
|
||||
|
||||
def omit_long_data(body, omit_len=512):
|
||||
""" omit too long str/bytes
|
||||
"""
|
||||
@@ -560,7 +534,7 @@ def omit_long_data(body, omit_len=512):
|
||||
return omitted_body + appendix_str
|
||||
|
||||
|
||||
def dump_json_file(json_data, json_file_abs_path):
|
||||
def dump_json_file(json_data: Union[dict, list], json_file_abs_path: str) -> None:
|
||||
""" dump json data to file
|
||||
"""
|
||||
class PythonObjectEncoder(json.JSONEncoder):
|
||||
@@ -593,11 +567,10 @@ def dump_json_file(json_data, json_file_abs_path):
|
||||
logger.error(msg)
|
||||
|
||||
|
||||
def prepare_log_file_abs_path(project_mapping, file_name):
|
||||
def prepare_log_file_abs_path(test_path: str, file_name: str) -> str:
|
||||
""" prepare dump json file absolute path.
|
||||
"""
|
||||
current_working_dir = os.getcwd()
|
||||
test_path = project_mapping.get("test_path")
|
||||
|
||||
if not test_path:
|
||||
# running passed in testcase/testsuite data structure
|
||||
@@ -619,17 +592,3 @@ def prepare_log_file_abs_path(project_mapping, file_name):
|
||||
|
||||
dumped_json_file_abs_path = os.path.join(file_foder_path, dump_file_name)
|
||||
return dumped_json_file_abs_path
|
||||
|
||||
|
||||
def dump_logs(json_data, project_mapping, tag_name):
|
||||
""" dump tests data to json file.
|
||||
the dumped file is located in PWD/logs folder.
|
||||
|
||||
Args:
|
||||
json_data (list/dict): json data to dump
|
||||
project_mapping (dict): project info
|
||||
tag_name (str): tag name, loaded/parsed/summary
|
||||
|
||||
"""
|
||||
json_file_abs_path = prepare_log_file_abs_path(project_mapping, f"{tag_name}.json")
|
||||
dump_json_file(json_data, json_file_abs_path)
|
||||
|
||||
@@ -278,30 +278,23 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
def test_prepare_dump_json_file_path_for_folder(self):
|
||||
# hrun tests/httpbin/a.b.c/ --save-tests
|
||||
project_mapping = {
|
||||
"test_path": os.path.join("tests", "httpbin", "a.b.c")
|
||||
}
|
||||
test_path = os.path.join("tests", "httpbin", "a.b.c")
|
||||
self.assertEqual(
|
||||
utils.prepare_log_file_abs_path(project_mapping, "loaded"),
|
||||
utils.prepare_log_file_abs_path(test_path, "loaded.json"),
|
||||
os.path.join(os.getcwd(), "logs", "tests/httpbin/a.b.c/all.loaded.json")
|
||||
)
|
||||
|
||||
def test_prepare_dump_json_file_path_for_file(self):
|
||||
# hrun tests/httpbin/a.b.c/rpc.yml --save-tests
|
||||
project_mapping = {
|
||||
"test_path": os.path.join("tests", "httpbin", "a.b.c", "rpc.yml")
|
||||
}
|
||||
test_path = os.path.join("tests", "httpbin", "a.b.c", "rpc.yml")
|
||||
self.assertEqual(
|
||||
utils.prepare_log_file_abs_path(project_mapping, "loaded"),
|
||||
utils.prepare_log_file_abs_path(test_path, "loaded.json"),
|
||||
os.path.join(os.getcwd(), "logs", "tests/httpbin/a.b.c/rpc.loaded.json")
|
||||
)
|
||||
|
||||
def test_prepare_dump_json_file_path_for_passed_testcase(self):
|
||||
project_working_directory = os.path.join(os.getcwd(), "tests")
|
||||
project_mapping = {
|
||||
"PWD": project_working_directory
|
||||
}
|
||||
test_path = ""
|
||||
self.assertEqual(
|
||||
utils.prepare_log_file_abs_path(project_mapping, "loaded"),
|
||||
utils.prepare_log_file_abs_path(test_path, "loaded.json"),
|
||||
os.path.join(os.getcwd(), "logs", "tests_mapping.loaded.json")
|
||||
)
|
||||
|
||||
@@ -66,7 +66,6 @@ nav:
|
||||
- 环境变量: prepare/dot-env.md
|
||||
- 测试用例分层: prepare/testcase-layer.md
|
||||
- 参数化数据驱动: prepare/parameters.md
|
||||
- Validate & Prettify: prepare/validate-pretty.md
|
||||
- 信息安全: prepare/security.md
|
||||
- 文件上传场景: prepare/upload-case.md
|
||||
- 测试执行:
|
||||
|
||||
Reference in New Issue
Block a user