From eccc992bd1c625b3a8cb599e4ec60b319de4d6dc Mon Sep 17 00:00:00 2001 From: lihuacai Date: Sat, 23 Sep 2023 18:03:03 +0800 Subject: [PATCH] feat(httprunner): return extractors to report --- httprunner/api.py | 5 +- httprunner/client.py | 5 +- httprunner/context.py | 3 +- httprunner/runner.py | 134 +++++++++++++++++++------- templates/report_template.html | 73 +++++++++++++- web/src/pages/reports/DebugReport.vue | 92 +++++++++++++++--- 6 files changed, 253 insertions(+), 59 deletions(-) diff --git a/httprunner/api.py b/httprunner/api.py index af83b8d..1c22be9 100644 --- a/httprunner/api.py +++ b/httprunner/api.py @@ -64,8 +64,9 @@ def test(self): finally: if hasattr(test_runner.http_client_session, "meta_data"): self.meta_data = test_runner.http_client_session.meta_data - self.meta_data["validators"] = test_runner.evaluated_validators - self.meta_data["logs"] = test_runner.context.logs + self.meta_data["validators"]: list[dict] = test_runner.evaluated_validators + self.meta_data["logs"]: list[str] = test_runner.context.logs + self.meta_data["extractors"]: list[dict] = test_runner.context.extractors # 赋值完之后,需要重新输出化http_client_session的meta数据,否则下次就会共享 test_runner.http_client_session.init_meta_data() diff --git a/httprunner/client.py b/httprunner/client.py index a19538c..95370bf 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -76,7 +76,10 @@ def init_meta_data(self): "encoding": None, "content": None, "content_type": "" - } + }, + "validators": [], + "logs": [], + "extractors": [], } def request(self, method, url, name=None, **kwargs): diff --git a/httprunner/context.py b/httprunner/context.py index 0609557..356787b 100644 --- a/httprunner/context.py +++ b/httprunner/context.py @@ -33,7 +33,8 @@ def __init__(self, variables=None, functions=None): self.evaluated_validators = [] self.init_context_variables(level="testcase") - self.logs = [] + self.logs: list[str] = [] + self.extractors: list[dict] = [] def init_context_variables(self, level="testcase"): """ initialize testcase/teststep context diff --git a/httprunner/runner.py b/httprunner/runner.py index 9f82973..3c77a89 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -11,7 +11,8 @@ from httprunner.compat import OrderedDict from httprunner.context import Context -logger = logging.getLogger('httprunner') +logger = logging.getLogger("httprunner") + class ListHandler(logging.Handler): def __init__(self, log_list: list): @@ -26,13 +27,39 @@ def emit(self, record): log_entry = self.format(record) self.log_list.append(log_entry) + +def _transform_to_list_of_dict(extractors: list[dict], extracted_variables_mapping: dict) -> list[dict]: + """transform extractors to list of dict. + + Args: + extractors (list): list of extractors + extracted_variables_mapping (dict): mapping between variable name and variable value + + Returns: + list: list of dict + + """ + if not extractors: + return [] + + result = [] + for extractor in extractors: + for key, value in extractor.items(): + extract_expr = value + actual_value = extracted_variables_mapping[key] + result.append({ + 'output_variable_name': key, + 'extract_expr': extract_expr, + 'actual_value': actual_value + }) + return result + + class Runner(object): # 每个线程对应Runner类的实例 instances = {} def __init__(self, config_dict=None, http_client_session=None): - """ - """ self.http_client_session = http_client_session config_dict = config_dict or {} self.evaluated_validators = [] @@ -61,7 +88,7 @@ def __del__(self): self.do_hook_actions(self.testcase_teardown_hooks) def init_test(self, test_dict, level): - """ create/update context variables binds + """create/update context variables binds Args: test_dict (dict): @@ -100,11 +127,12 @@ def init_test(self, test_dict, level): test_dict = utils.lower_test_dict_keys(test_dict) self.context.init_context_variables(level) - variables = test_dict.get('variables') \ - or test_dict.get('variable_binds', OrderedDict()) + variables = test_dict.get("variables") or test_dict.get( + "variable_binds", OrderedDict() + ) self.context.update_context_variables(variables, level) - request_config = test_dict.get('request', {}) + request_config = test_dict.get("request", {}) parsed_request = self.context.get_parsed_request(request_config, level) base_url = parsed_request.pop("base_url", None) @@ -113,7 +141,7 @@ def init_test(self, test_dict, level): return parsed_request def _handle_skip_feature(self, teststep_dict): - """ handle skip feature for teststep + """handle skip feature for teststep - skip: skip current test unconditionally - skipIf: skip current test if condition is true - skipUnless: skip current test unless condition is true @@ -146,12 +174,12 @@ def _handle_skip_feature(self, teststep_dict): def do_hook_actions(self, actions): for action in actions: - logger.info("call hook: %s",action) + logger.info("call hook: %s", action) # TODO: check hook function if valid self.context.eval_content(action) def run_test(self, teststep_dict): - """ run single teststep. + """run single teststep. Args: teststep_dict (dict): teststep info @@ -185,16 +213,21 @@ def run_test(self, teststep_dict): all_logs: list[str] = [] list_handler = ListHandler(all_logs) self.context.logs = [] + self.context.extractors = [] try: - # 临时添加自定义处理器 + # 临时添加自定义处理器 logger.addHandler(list_handler) # check skip self._handle_skip_feature(teststep_dict) # prepare - extractors = teststep_dict.get("extract", []) or teststep_dict.get("extractors", []) - validators = teststep_dict.get("validate", []) or teststep_dict.get("validators", []) + extractors = teststep_dict.get("extract", []) or teststep_dict.get( + "extractors", [] + ) + validators = teststep_dict.get("validate", []) or teststep_dict.get( + "validators", [] + ) parsed_request = self.init_test(teststep_dict, level="teststep") self.context.update_teststep_variables_mapping("request", parsed_request) @@ -207,14 +240,18 @@ def run_test(self, teststep_dict): logger.info("execute setup hooks end") # 计算前置setup_hooks消耗的时间 setup_hooks_duration = 0 - self.http_client_session.meta_data['request']['setup_hooks_start'] = setup_hooks_start + self.http_client_session.meta_data["request"][ + "setup_hooks_start" + ] = setup_hooks_start if len(setup_hooks) > 1: setup_hooks_duration = time.time() - setup_hooks_start - self.http_client_session.meta_data['request']['setup_hooks_duration'] = setup_hooks_duration + self.http_client_session.meta_data["request"][ + "setup_hooks_duration" + ] = setup_hooks_duration try: - url = parsed_request.pop('url') - method = parsed_request.pop('method') + url = parsed_request.pop("url") + method = parsed_request.pop("method") group_name = parsed_request.pop("group", None) except KeyError: raise exceptions.ParamsError("URL or METHOD missed!") @@ -222,7 +259,7 @@ def run_test(self, teststep_dict): # TODO: move method validation to json schema valid_methods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] if method.upper() not in valid_methods: - err_msg = u"Invalid HTTP method! => {}\n".format(method) + err_msg = "Invalid HTTP method! => {}\n".format(method) err_msg += "Available HTTP methods: {}".format("/".join(valid_methods)) logger.error(err_msg) raise exceptions.ParamsError(err_msg) @@ -230,16 +267,13 @@ def run_test(self, teststep_dict): logger.info("{method} {url}".format(method=method, url=url)) logger.debug("request kwargs(raw): {kwargs}".format(kwargs=parsed_request)) - user_timeout: str = str(pydash.get(parsed_request, 'headers.timeout')) + user_timeout: str = str(pydash.get(parsed_request, "headers.timeout")) if user_timeout and user_timeout.isdigit(): - parsed_request['timeout'] = int(user_timeout) + parsed_request["timeout"] = int(user_timeout) # request resp = self.http_client_session.request( - method, - url, - name=group_name, - **parsed_request + method, url, name=group_name, **parsed_request ) resp_obj = response.ResponseObject(resp) @@ -250,22 +284,48 @@ def run_test(self, teststep_dict): teardown_hooks_start = time.time() if teardown_hooks: logger.info("start to run teardown hooks") - logger.info("update_teststep_variables_mapping, response: %s", resp_obj.resp_obj.text) + logger.info( + "update_teststep_variables_mapping, response: %s", + resp_obj.resp_obj.text, + ) self.context.update_teststep_variables_mapping("response", resp_obj) self.do_hook_actions(teardown_hooks) teardown_hooks_duration = time.time() - teardown_hooks_start - logger.info("run teardown hooks end, duration: %s", teardown_hooks_duration) - self.http_client_session.meta_data['response']['teardown_hooks_start'] = teardown_hooks_start - self.http_client_session.meta_data['response']['teardown_hooks_duration'] = teardown_hooks_duration + logger.info( + "run teardown hooks end, duration: %s", teardown_hooks_duration + ) + self.http_client_session.meta_data["response"][ + "teardown_hooks_start" + ] = teardown_hooks_start + self.http_client_session.meta_data["response"][ + "teardown_hooks_duration" + ] = teardown_hooks_duration # extract - extracted_variables_mapping = resp_obj.extract_response(extractors, self.context) - self.context.update_testcase_runtime_variables_mapping(extracted_variables_mapping) + extracted_variables_mapping = resp_obj.extract_response( + extractors, self.context + ) + self.context.extractors = _transform_to_list_of_dict(extractors, extracted_variables_mapping) + logger.info( + "source testcase_runtime_variables_mapping: %s", + dict(self.context.testcase_runtime_variables_mapping), + ) + logger.info( + "source testcase_runtime_variables_mapping update with: %s", + dict(extracted_variables_mapping) + ) + self.context.update_testcase_runtime_variables_mapping( + extracted_variables_mapping + ) # validate try: self.evaluated_validators = self.context.validate(validators, resp_obj) - except (exceptions.ParamsError, exceptions.ValidationFailure, exceptions.ExtractFailure): + except ( + exceptions.ParamsError, + exceptions.ValidationFailure, + exceptions.ExtractFailure, + ): # log request err_req_msg = "request: \n" err_req_msg += "headers: {}\n".format(parsed_request.pop("headers", {})) @@ -286,16 +346,16 @@ def run_test(self, teststep_dict): logger.removeHandler(list_handler) def extract_output(self, output_variables_list): - """ extract output variables - """ + """extract output variables""" variables_mapping = self.context.teststep_variables_mapping output = {} for variable in output_variables_list: if variable not in variables_mapping: logger.warning( - "variable '{}' can not be found in variables mapping, failed to output!"\ - .format(variable) + "variable '{}' can not be found in variables mapping, failed to output!".format( + variable + ) ) continue @@ -328,7 +388,9 @@ def set_config_header(name, value): # 在运行时修改配置中请求头的信息 # 比如: 用例中需要切换账号,实现同时请求头中token和userId current_context = Hrun.get_current_context() - pydash.set_(current_context.TESTCASE_SHARED_REQUEST_MAPPING, f'headers.{name}', value) + pydash.set_( + current_context.TESTCASE_SHARED_REQUEST_MAPPING, f"headers.{name}", value + ) @staticmethod def set_step_var(name, value): diff --git a/templates/report_template.html b/templates/report_template.html index 6951268..cbc009d 100644 --- a/templates/report_template.html +++ b/templates/report_template.html @@ -13,7 +13,6 @@ {% load custom_tags %} - {{ html_report_name }} - 测试报告 @@ -8205,9 +8210,69 @@
Test Cases
class='material-icons'>low_priority Validators - {% for validator in record.meta_data.validators %} -
check-{{ validator.desc }}: {{ validator.check }} - {{ validator.comparator }}:[{{ validator.expect }}, {{ validator.check_value }}]
- {% endfor %} + {% if not props.row.meta_data.extractors %} + 哦豁,并没有validator,快去加一个吧,没有断言的用例不够健壮哦 + {% else %} + + + + + + + + + + + + + {% for validator in record.meta_data.validators %} + + + + + + + + + {% endfor %} + +
是否通过 + + 取值表达式 + 实际值比较器期望值描述
{{ validator.check_result }}{{ validator.check }}{{ validator.check_value }}{{ validator.comparator }}{{ validator.expect }}{{ validator.desc }}
+ {% endif %} + + + + low_priority + Extractors + + {% if not props.row.meta_data.extractors %} + 哦豁,运行的时候并没有extractor + {% else %} + + + + + + + + + + {% for extractor in props.row.meta_data.extractors %} + + + + + + {% endfor %} + +
+ 取值表达式 + 实际值输出的变量名
{{ extractor.extract_expr }}{{ extractor.actual_value }}{{ extractor.output_variable_name }}
+ {% endif %} + diff --git a/web/src/pages/reports/DebugReport.vue b/web/src/pages/reports/DebugReport.vue index d91e4c8..3ecde15 100644 --- a/web/src/pages/reports/DebugReport.vue +++ b/web/src/pages/reports/DebugReport.vue @@ -106,23 +106,65 @@

                             
- - - - - - - - - - - - - + + + - -

+
+                            
+                                
+                                
                             
+
                             
                                 
                                     
                                 
                             
+
+                            
+                                
+                                
+                            
                         
                     
                 
@@ -385,4 +438,13 @@ pre {
     white-space: pre-wrap;
     word-wrap: break-word;
 }
+
+.centered-content {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%; /* 根据你的需要调整高度 */
+    width: 100%; /* 根据你的需要调整宽度 */
+    text-align: center;
+}