mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-03 13:31:26 +08:00
fix skip错误和增加mark功能和增加meta功能收集用例 (#1733)
* fix skip错误和增加mark功能和增加meta功能收集用例 * 增加自定义验证器 * 在allure报告中增加运行日志文件 * 测试用例元数据收集增加用例文件路径 * 增加公司的全链路追踪ID * allure测试报告增加用例repo地址link
This commit is contained in:
@@ -217,12 +217,35 @@ def ensure_testcase_v3_api(api_content: Dict) -> Dict:
|
|||||||
|
|
||||||
def ensure_testcase_v3(test_content: Dict) -> Dict:
|
def ensure_testcase_v3(test_content: Dict) -> Dict:
|
||||||
logger.info("ensure compatibility with testcase format v2")
|
logger.info("ensure compatibility with testcase format v2")
|
||||||
|
logger.info(f"test_content: {test_content}")
|
||||||
v3_content = {"config": test_content["config"], "teststeps": []}
|
v3_content = {"config": test_content["config"], "teststeps": [], "meta": {"path": ""}}
|
||||||
|
|
||||||
if "teststeps" not in test_content:
|
if "teststeps" not in test_content:
|
||||||
logger.error(f"Miss teststeps: {test_content}")
|
logger.error(f"Miss teststeps: {test_content}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 分析test_content["config"]中的config,如果没有meta
|
||||||
|
if "meta" not in test_content['config']:
|
||||||
|
logger.error(f"Miss meta test_file: {test_content['config']['path']}")
|
||||||
|
logger.error(f"file config content : {test_content['config']}")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
# 判断mata中是否有author,description,service,controller没有就抛错
|
||||||
|
if "author" not in test_content['config']['meta']:
|
||||||
|
logger.error(f"Miss author: {test_content}")
|
||||||
|
sys.exit(1)
|
||||||
|
if "description" not in test_content['config']['meta']:
|
||||||
|
logger.error(f"Miss description: {test_content}")
|
||||||
|
sys.exit(1)
|
||||||
|
if "service" not in test_content['config']['meta']:
|
||||||
|
logger.error(f"Miss service: {test_content}")
|
||||||
|
sys.exit(1)
|
||||||
|
if "controller" not in test_content['config']['meta']:
|
||||||
|
logger.error(f"Miss controller: {test_content}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
v3_content["meta"] = test_content['config']['meta']
|
||||||
|
v3_content["meta"]["path"] = convert_relative_project_root_dir(test_content['config']['path'])
|
||||||
|
|
||||||
if not isinstance(test_content["teststeps"], list):
|
if not isinstance(test_content["teststeps"], list):
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|||||||
@@ -443,5 +443,4 @@ def convert_relative_project_root_dir(abs_path: Text) -> Text:
|
|||||||
f"abs_path: {abs_path}\n"
|
f"abs_path: {abs_path}\n"
|
||||||
f"project_meta.RootDir: {_project_meta.RootDir}"
|
f"project_meta.RootDir: {_project_meta.RootDir}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return abs_path[len(_project_meta.RootDir) + 1:]
|
return abs_path[len(_project_meta.RootDir) + 1:]
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ pytest_files_made_cache_mapping: Dict[Text, Text] = {}
|
|||||||
""" save generated pytest files to run, except referenced testcase
|
""" save generated pytest files to run, except referenced testcase
|
||||||
"""
|
"""
|
||||||
pytest_files_run_set: Set = set()
|
pytest_files_run_set: Set = set()
|
||||||
|
"""用来记录make之后的所有case 元数据,做统计使用"""
|
||||||
|
pytest_files_meta_cases_list: List = []
|
||||||
|
|
||||||
__TEMPLATE__ = jinja2.Template(
|
__TEMPLATE__ = jinja2.Template(
|
||||||
"""# NOTE: Generated By HttpRunner v{{ version }}
|
"""# NOTE: Generated By HttpRunner v{{ version }}
|
||||||
@@ -36,7 +38,7 @@ from pathlib import Path
|
|||||||
sys.path.insert(0, str(Path(__file__){% for _ in range(diff_levels) %}.parent{% endfor %}))
|
sys.path.insert(0, str(Path(__file__){% for _ in range(diff_levels) %}.parent{% endfor %}))
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if parameters or skip %}
|
{% if parameters or skip or marks %}
|
||||||
import pytest
|
import pytest
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -50,9 +52,20 @@ from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
class {{ class_name }}(HttpRunner):
|
class {{ class_name }}(HttpRunner):
|
||||||
{% if parameters and skip %}
|
{% if parameters and skip and marks%}
|
||||||
@pytest.mark.parametrize("param", Parameters({{parameters}}))
|
@pytest.mark.parametrize("param", Parameters({{parameters}}))
|
||||||
@pytest.mark.skip(reason={{ skip }})
|
@pytest.mark.skip(reason="{{ skip }}")
|
||||||
|
{% for mark in marks %}
|
||||||
|
@pytest.mark.{{ mark }}
|
||||||
|
{% endfor %}
|
||||||
|
def test_start(self, param):
|
||||||
|
super().test_start(param)
|
||||||
|
|
||||||
|
{% elif parameters and marks %}
|
||||||
|
@pytest.mark.parametrize("param", Parameters({{parameters}}))
|
||||||
|
{% for mark in marks %}
|
||||||
|
@pytest.mark.{{ mark }}
|
||||||
|
{% endfor %}
|
||||||
def test_start(self, param):
|
def test_start(self, param):
|
||||||
super().test_start(param)
|
super().test_start(param)
|
||||||
|
|
||||||
@@ -61,8 +74,15 @@ class {{ class_name }}(HttpRunner):
|
|||||||
def test_start(self, param):
|
def test_start(self, param):
|
||||||
super().test_start(param)
|
super().test_start(param)
|
||||||
|
|
||||||
|
{% elif marks %}
|
||||||
|
{% for mark in marks %}
|
||||||
|
@pytest.mark.{{ mark }}
|
||||||
|
{% endfor %}
|
||||||
|
def test_start(self):
|
||||||
|
super().test_start()
|
||||||
|
|
||||||
{% elif skip %}
|
{% elif skip %}
|
||||||
@pytest.mark.skip(reason={{ skip }})
|
@pytest.mark.skip(reason="{{ skip }}")
|
||||||
def test_start(self):
|
def test_start(self):
|
||||||
super().test_start()
|
super().test_start()
|
||||||
|
|
||||||
@@ -344,10 +364,18 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
|
|||||||
expect = f'"{expect}"'
|
expect = f'"{expect}"'
|
||||||
|
|
||||||
message = validator["message"]
|
message = validator["message"]
|
||||||
if message:
|
|
||||||
step_info += f".assert_{assert_method}({check}, {expect}, '{message}')"
|
if assert_method == "custom":
|
||||||
|
custom_comparator = validator['custom_comparator']
|
||||||
|
if message:
|
||||||
|
step_info += f".assert_{assert_method}('{custom_comparator}',{check}, {expect}, '{message}')"
|
||||||
|
else:
|
||||||
|
step_info += f".assert_{assert_method}('{custom_comparator}',{check}, {expect})"
|
||||||
else:
|
else:
|
||||||
step_info += f".assert_{assert_method}({check}, {expect})"
|
if message:
|
||||||
|
step_info += f".assert_{assert_method}({check}, {expect}, '{message}')"
|
||||||
|
else:
|
||||||
|
step_info += f".assert_{assert_method}({check}, {expect})"
|
||||||
|
|
||||||
return f"Step({step_info})"
|
return f"Step({step_info})"
|
||||||
|
|
||||||
@@ -356,7 +384,8 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
|
|||||||
"""convert valid testcase dict to pytest file path"""
|
"""convert valid testcase dict to pytest file path"""
|
||||||
# ensure compatibility with testcase format v2
|
# ensure compatibility with testcase format v2
|
||||||
testcase = ensure_testcase_v3(testcase)
|
testcase = ensure_testcase_v3(testcase)
|
||||||
|
|
||||||
|
pytest_files_meta_cases_list.append(testcase['meta'])
|
||||||
# validate testcase format
|
# validate testcase format
|
||||||
load_testcase(testcase)
|
load_testcase(testcase)
|
||||||
|
|
||||||
@@ -441,10 +470,10 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
|
|||||||
"teststeps_chain_style": [
|
"teststeps_chain_style": [
|
||||||
make_teststep_chain_style(step) for step in teststeps
|
make_teststep_chain_style(step) for step in teststeps
|
||||||
],
|
],
|
||||||
|
"marks": config.get("marks", []),
|
||||||
}
|
}
|
||||||
logger.info(make_config_skip(config))
|
logger.info(make_config_skip(config))
|
||||||
content = __TEMPLATE__.render(data)
|
content = __TEMPLATE__.render(data)
|
||||||
|
|
||||||
# ensure new file's directory exists
|
# ensure new file's directory exists
|
||||||
dir_path = os.path.dirname(testcase_python_abs_path)
|
dir_path = os.path.dirname(testcase_python_abs_path)
|
||||||
if not os.path.exists(dir_path):
|
if not os.path.exists(dir_path):
|
||||||
@@ -526,7 +555,6 @@ def __make(tests_path: Text):
|
|||||||
tests_path: should be in absolute path
|
tests_path: should be in absolute path
|
||||||
|
|
||||||
"""
|
"""
|
||||||
logger.info(f"make path: {tests_path}")
|
|
||||||
test_files = []
|
test_files = []
|
||||||
if os.path.isdir(tests_path):
|
if os.path.isdir(tests_path):
|
||||||
files_list = load_folder_files(tests_path)
|
files_list = load_folder_files(tests_path)
|
||||||
@@ -624,6 +652,11 @@ def main_make(tests_paths: List[Text]) -> List[Text]:
|
|||||||
pytest_files_format_list = pytest_files_made_cache_mapping.keys()
|
pytest_files_format_list = pytest_files_made_cache_mapping.keys()
|
||||||
format_pytest_with_black(*pytest_files_format_list)
|
format_pytest_with_black(*pytest_files_format_list)
|
||||||
|
|
||||||
|
# 打印收集到的case数据
|
||||||
|
logger.info(f"pytest_files_meta_cases_list: {pytest_files_meta_cases_list}")
|
||||||
|
import json
|
||||||
|
with open('pytest_files_meta_cases_list.json', 'w') as f:
|
||||||
|
json.dump(pytest_files_meta_cases_list, f)
|
||||||
return list(pytest_files_run_set)
|
return list(pytest_files_run_set)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -88,30 +88,47 @@ def uniform_validator(validator):
|
|||||||
# format2
|
# format2
|
||||||
comparator = list(validator.keys())[0]
|
comparator = list(validator.keys())[0]
|
||||||
compare_values = validator[comparator]
|
compare_values = validator[comparator]
|
||||||
|
if comparator == "custom":
|
||||||
if not isinstance(compare_values, list) or len(compare_values) not in [2, 3]:
|
custom_comparator = compare_values[0]
|
||||||
raise ParamsError(f"invalid validator: {validator}")
|
check_item = compare_values[1]
|
||||||
|
expect_value = compare_values[2]
|
||||||
check_item = compare_values[0]
|
if len(compare_values) == 4:
|
||||||
expect_value = compare_values[1]
|
message = compare_values[3]
|
||||||
if len(compare_values) == 3:
|
else:
|
||||||
message = compare_values[2]
|
# len(compare_values) == 2
|
||||||
|
message = ""
|
||||||
else:
|
else:
|
||||||
# len(compare_values) == 2
|
if not isinstance(compare_values, list) or len(compare_values) not in [2, 3]:
|
||||||
message = ""
|
raise ParamsError(f"invalid validator: {validator}")
|
||||||
|
|
||||||
|
check_item = compare_values[0]
|
||||||
|
expect_value = compare_values[1]
|
||||||
|
if len(compare_values) == 3:
|
||||||
|
message = compare_values[2]
|
||||||
|
else:
|
||||||
|
# len(compare_values) == 2
|
||||||
|
message = ""
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ParamsError(f"invalid validator: {validator}")
|
raise ParamsError(f"invalid validator: {validator}")
|
||||||
|
|
||||||
# uniform comparator, e.g. lt => less_than, eq => equals
|
# uniform comparator, e.g. lt => less_than, eq => equals
|
||||||
assert_method = get_uniform_comparator(comparator)
|
assert_method = get_uniform_comparator(comparator)
|
||||||
|
if assert_method == "custom":
|
||||||
return {
|
return {
|
||||||
"check": check_item,
|
"check": check_item,
|
||||||
"expect": expect_value,
|
"expect": expect_value,
|
||||||
"assert": assert_method,
|
"assert": assert_method,
|
||||||
"message": message,
|
"message": message,
|
||||||
}
|
"custom_comparator": custom_comparator,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"check": check_item,
|
||||||
|
"expect": expect_value,
|
||||||
|
"assert": assert_method,
|
||||||
|
"message": message,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ResponseObject(object):
|
class ResponseObject(object):
|
||||||
@@ -194,7 +211,6 @@ class ResponseObject(object):
|
|||||||
variables_mapping: VariablesMapping = None,
|
variables_mapping: VariablesMapping = None,
|
||||||
functions_mapping: FunctionsMapping = None,
|
functions_mapping: FunctionsMapping = None,
|
||||||
):
|
):
|
||||||
|
|
||||||
variables_mapping = variables_mapping or {}
|
variables_mapping = variables_mapping or {}
|
||||||
functions_mapping = functions_mapping or {}
|
functions_mapping = functions_mapping or {}
|
||||||
|
|
||||||
@@ -206,12 +222,10 @@ class ResponseObject(object):
|
|||||||
failures = []
|
failures = []
|
||||||
|
|
||||||
for v in validators:
|
for v in validators:
|
||||||
|
|
||||||
if "validate_extractor" not in self.validation_results:
|
if "validate_extractor" not in self.validation_results:
|
||||||
self.validation_results["validate_extractor"] = []
|
self.validation_results["validate_extractor"] = []
|
||||||
|
|
||||||
u_validator = uniform_validator(v)
|
u_validator = uniform_validator(v)
|
||||||
|
|
||||||
# check item
|
# check item
|
||||||
check_item = u_validator["check"]
|
check_item = u_validator["check"]
|
||||||
if "$" in check_item:
|
if "$" in check_item:
|
||||||
@@ -229,7 +243,11 @@ class ResponseObject(object):
|
|||||||
|
|
||||||
# comparator
|
# comparator
|
||||||
assert_method = u_validator["assert"]
|
assert_method = u_validator["assert"]
|
||||||
assert_func = get_mapping_function(assert_method, functions_mapping)
|
if assert_method != 'custom':
|
||||||
|
assert_func = get_mapping_function(assert_method, functions_mapping)
|
||||||
|
else:
|
||||||
|
assert_method = u_validator["custom_comparator"]
|
||||||
|
assert_func = get_mapping_function(assert_method, functions_mapping)
|
||||||
|
|
||||||
# expect item
|
# expect item
|
||||||
expect_item = u_validator["expect"]
|
expect_item = u_validator["expect"]
|
||||||
|
|||||||
@@ -145,9 +145,16 @@ class HttpRunner(object):
|
|||||||
parsed_request_dict = parse_data(
|
parsed_request_dict = parse_data(
|
||||||
request_dict, step.variables, self.__project_meta.functions
|
request_dict, step.variables, self.__project_meta.functions
|
||||||
)
|
)
|
||||||
|
case_id = f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}"
|
||||||
|
# 增加x-trace-id
|
||||||
|
parsed_request_dict["headers"].setdefault(
|
||||||
|
"x-trace-id",
|
||||||
|
case_id,
|
||||||
|
)
|
||||||
|
|
||||||
parsed_request_dict["headers"].setdefault(
|
parsed_request_dict["headers"].setdefault(
|
||||||
"HRUN-Request-ID",
|
"HRUN-Request-ID",
|
||||||
f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}",
|
case_id,
|
||||||
)
|
)
|
||||||
step.variables["request"] = parsed_request_dict
|
step.variables["request"] = parsed_request_dict
|
||||||
|
|
||||||
@@ -446,7 +453,14 @@ class HttpRunner(object):
|
|||||||
# update allure report meta
|
# update allure report meta
|
||||||
allure.dynamic.title(self.__config.name)
|
allure.dynamic.title(self.__config.name)
|
||||||
allure.dynamic.description(f"TestCase ID: {self.__case_id}")
|
allure.dynamic.description(f"TestCase ID: {self.__case_id}")
|
||||||
|
git_repo = self.__project_meta.env.get("git_repo", '')
|
||||||
|
file_ext = self.__project_meta.env.get("file_ext", '')
|
||||||
|
if git_repo != '' and file_ext != '':
|
||||||
|
from httprunner.loader import convert_relative_project_root_dir
|
||||||
|
# 由于运行时加载的是py文件,所以这里link的时候得做一下替换
|
||||||
|
link = git_repo + convert_relative_project_root_dir(self.__config.path)[:-8] + '.' +file_ext
|
||||||
|
allure.dynamic.link(link)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Start to run testcase: {self.__config.name}, TestCase ID: {self.__case_id}"
|
f"Start to run testcase: {self.__config.name}, TestCase ID: {self.__case_id}"
|
||||||
)
|
)
|
||||||
@@ -456,5 +470,6 @@ class HttpRunner(object):
|
|||||||
TestCase(config=self.__config, teststeps=self.__teststeps)
|
TestCase(config=self.__config, teststeps=self.__teststeps)
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
allure.attach.file(self.__log_path, name="log", attachment_type=allure.attachment_type.TEXT)
|
||||||
logger.remove(log_handler)
|
logger.remove(log_handler)
|
||||||
logger.info(f"generate testcase log: {self.__log_path}")
|
logger.info(f"generate testcase log: {self.__log_path}")
|
||||||
|
|||||||
@@ -213,6 +213,14 @@ class StepRequestValidation(object):
|
|||||||
{"type_match": [jmes_path, expected_value, message]}
|
{"type_match": [jmes_path, expected_value, message]}
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def assert_custom(
|
||||||
|
self, custom_comparator, jmes_path: Text, expected_value: Any, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step_context.validators.append(
|
||||||
|
{"custom": [custom_comparator, jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
def perform(self) -> TStep:
|
||||||
return self.__step_context
|
return self.__step_context
|
||||||
|
|||||||
Reference in New Issue
Block a user