diff --git a/httprunner/__about__.py b/httprunner/__about__.py index 211c90c2..50405f44 100644 --- a/httprunner/__about__.py +++ b/httprunner/__about__.py @@ -1,7 +1,7 @@ __title__ = 'HttpRunner' __description__ = 'One-stop solution for HTTP(S) testing.' __url__ = 'https://github.com/HttpRunner/HttpRunner' -__version__ = '1.5.15' +__version__ = '1.6.0.alpha' __author__ = 'debugtalk' __author_email__ = 'mail@debugtalk.com' __license__ = 'MIT' diff --git a/httprunner/loader.py b/httprunner/loader.py index d8294c40..82aa5b4b 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -1,4 +1,5 @@ import collections +import copy import csv import importlib import io @@ -10,7 +11,6 @@ import yaml from httprunner import built_in, exceptions, logger, parser, utils, validator from httprunner.compat import OrderedDict - ############################################################################### ## file loader ############################################################################### @@ -235,6 +235,7 @@ def load_python_module(module): } """ + # TODO (2.0): remove variables from debugtalk.py debugtalk_module = { "variables": {}, "functions": {} @@ -324,46 +325,46 @@ def _load_teststeps(test_block, project_mapping): { "name": "add product to cart", "api": "api_add_cart()", - "validate": [] + "variables": [], + "validate": [], + "extract": [] } # testcase reference { "name": "add product to cart", "suite": "create_and_check()", - "validate": [] + "variables": [] } # define directly { "name": "checkout cart", "request": {}, - "validate": [] + "variables": [], + "validate": [], + "extract": [] } Returns: list: loaded teststeps list """ - def extend_api_definition(block): - ref_call = block["api"] - def_block = _get_block_by_name(ref_call, "def-api", project_mapping) - _extend_block(block, def_block) - teststeps = [] # reference api if "api" in test_block: - extend_api_definition(test_block) - teststeps.append(test_block) + ref_call = test_block.pop("api") + def_block = _get_block_by_name(ref_call, "def-api", project_mapping) + extended_block = _extend_block(test_block, def_block) + teststeps.append(extended_block) # reference testcase elif "suite" in test_block: # TODO: replace suite with testcase - ref_call = test_block["suite"] - block = _get_block_by_name(ref_call, "def-testcase", project_mapping) + ref_call = test_block.pop("suite") + def_block = _get_block_by_name(ref_call, "def-testcase", project_mapping) # TODO: bugfix lost block config variables - for teststep in block["teststeps"]: - if "api" in teststep: - extend_api_definition(teststep) - teststeps.append(teststep) + for teststep in def_block["teststeps"]: + _teststeps = _load_teststeps(teststep, project_mapping) + teststeps.extend(_teststeps) # define directly else: @@ -491,6 +492,8 @@ def _get_test_definition(name, ref_type, project_mapping): """ block = project_mapping.get(ref_type, {}).get(name) + # NOTICE: avoid project_mapping been changed during iteration. + block = copy.deepcopy(block) if not block: err_msg = "{} not found!".format(name) @@ -504,7 +507,7 @@ def _get_test_definition(name, ref_type, project_mapping): def _extend_block(ref_block, def_block): - """ extend ref_block with def_block. + """ extend ref_block with def_block, ref_block will merge and override def_block. Args: def_block (dict): api definition dict. @@ -533,27 +536,59 @@ def _extend_block(ref_block, def_block): } """ - # TODO: override variables - def_validators = def_block.get("validate") or def_block.get("validators", []) - ref_validators = ref_block.get("validate") or ref_block.get("validators", []) + extended_block = {} - def_extrators = def_block.get("extract") \ - or def_block.get("extractors") \ - or def_block.get("extract_binds", []) - ref_extractors = ref_block.get("extract") \ - or ref_block.get("extractors") \ - or ref_block.get("extract_binds", []) + # override name + extended_block["name"] = ref_block.pop("name", None) or def_block.pop("name", "") - ref_block.update(def_block) - ref_block["validate"] = _merge_validator( + # override variables + def_variables = def_block.pop("variables", []) + ref_variables = ref_block.pop("variables", []) + extended_block["variables"] = _extend_variables( + def_variables, + ref_variables + ) + + # merge & override validators + def_validators = def_block.pop("validate", None) or def_block.pop("validators", []) + ref_validators = ref_block.pop("validate", None) or ref_block.pop("validators", []) + extended_block["validate"] = _extend_validators( def_validators, ref_validators ) - ref_block["extract"] = _merge_extractor( + + # merge & override extractors + def_extrators = def_block.pop("extract", None) \ + or def_block.pop("extractors", None) \ + or def_block.pop("extract_binds", []) + ref_extractors = ref_block.pop("extract", None) \ + or ref_block.pop("extractors", None) \ + or ref_block.pop("extract_binds", []) + extended_block["extract"] = _extend_variables( def_extrators, ref_extractors ) + # TODO: merge & override request + def_request = def_block.pop("request", {}) + ref_request = ref_block.pop("request", {}) + extended_block["request"] = def_request + + # merge & override setup_hooks + def_setup_hooks = def_block.pop("setup_hooks", []) + ref_setup_hooks = ref_block.pop("setup_hooks", []) + extended_block["setup_hooks"] = list(set(def_setup_hooks + ref_setup_hooks)) + # merge & override teardown_hooks + def_teardown_hooks = def_block.pop("teardown_hooks", []) + ref_teardown_hooks = ref_block.pop("teardown_hooks", []) + extended_block["teardown_hooks"] = list(set(def_teardown_hooks + ref_teardown_hooks)) + + # TODO: extend with other ref block items, e.g. times + # extended_block.update(def_block) + extended_block.update(ref_block) + + return extended_block + def _convert_validators_to_mapping(validators): """ convert validators list to mapping. @@ -592,20 +627,21 @@ def _convert_validators_to_mapping(validators): return validators_mapping -def _merge_validator(def_validators, ref_validators): - """ merge def_validators with ref_validators. +def _extend_validators(def_validators, ref_validators): + """ extend ref_validators with def_validators. + ref_validators will merge and override def_validators. Args: def_validators (list): ref_validators (list): Returns: - list: merged validators + list: extended validators Examples: >>> def_validators = [{'eq': ['v1', 200]}, {"check": "s2", "expect": 16, "comparator": "len_eq"}] >>> ref_validators = [{"check": "v1", "expect": 201}, {'len_eq': ['s3', 12]}] - >>> _merge_validator(def_validators, ref_validators) + >>> _extend_validators(def_validators, ref_validators) [ {"check": "v1", "expect": 201, "comparator": "eq"}, {"check": "s2", "expect": 16, "comparator": "len_eq"}, @@ -627,20 +663,21 @@ def _merge_validator(def_validators, ref_validators): return list(def_validators_mapping.values()) -def _merge_extractor(def_extrators, ref_extractors): - """ merge def_extrators with ref_extractors +def _extend_variables(def_variables, ref_variables): + """ extend ref_variables with def_variables. + ref_variables will merge and override def_variables. Args: - def_extrators (list): [{"var1": "val1"}, {"var2": "val2"}] - ref_extractors (list): [{"var1": "val111"}, {"var3": "val3"}] + def_variables (list): + ref_variables (list): Returns: - list: merged extractors + list: extended variables Examples: - >>> def_extrators = [{"var1": "val1"}, {"var2": "val2"}] - >>> ref_extractors = [{"var1": "val111"}, {"var3": "val3"}] - >>> _merge_extractor(def_extrators, ref_extractors) + >>> def_variables = [{"var1": "val1"}, {"var2": "val2"}] + >>> ref_variables = [{"var1": "val111"}, {"var3": "val3"}] + >>> _extend_variables(def_variables, ref_variables) [ {"var1": "val111"}, {"var2": "val2"}, @@ -648,35 +685,29 @@ def _merge_extractor(def_extrators, ref_extractors): ] """ - if not def_extrators: - return ref_extractors + if not def_variables: + return ref_variables - elif not ref_extractors: - return def_extrators + elif not ref_variables: + return def_variables else: - extractor_dict = OrderedDict() - for api_extrator in def_extrators: - if len(api_extrator) != 1: - logger.log_warning("incorrect extractor: {}".format(api_extrator)) + extended_variables_dict = OrderedDict() + for def_variable in def_variables: + var_name = list(def_variable.keys())[0] + extended_variables_dict[var_name] = def_variable[var_name] + + for ref_variable in ref_variables: + if not ref_variable: continue + var_name = list(ref_variable.keys())[0] + extended_variables_dict[var_name] = ref_variable[var_name] - var_name = list(api_extrator.keys())[0] - extractor_dict[var_name] = api_extrator[var_name] + extended_variables = [] + for key, value in extended_variables_dict.items(): + extended_variables.append({key: value}) - for test_extrator in ref_extractors: - if len(test_extrator) != 1: - logger.log_warning("incorrect extractor: {}".format(test_extrator)) - continue - - var_name = list(test_extrator.keys())[0] - extractor_dict[var_name] = test_extrator[var_name] - - extractor_list = [] - for key, value in extractor_dict.items(): - extractor_list.append({key: value}) - - return extractor_list + return extended_variables def load_folder_content(folder_path): @@ -743,6 +774,7 @@ def load_api_folder(api_folder_path): } """ + # TODO: refactor api storage format, use one file for each api. api_definition_mapping = {} api_items_mapping = load_folder_content(api_folder_path) @@ -752,6 +784,7 @@ def load_api_folder(api_folder_path): for api_item in api_items: key, api_dict = api_item.popitem() + # TODO: replace def with api file path api_def = api_dict.pop("def") function_meta = parser.parse_function(api_def) func_name = function_meta["func_name"] @@ -826,6 +859,7 @@ def load_test_folder(test_folder_path): test_definition_mapping[test_file_path] = testcase continue + # TODO: replace def with testcase file path testcase_def = block.pop("def") function_meta = parser.parse_function(testcase_def) func_name = function_meta["func_name"] @@ -837,6 +871,7 @@ def load_test_folder(test_folder_path): test_definition_mapping[func_name] = testcase else: # key == "test": + ### TODO: extend suite with api testcase["teststeps"].append(block) return test_definition_mapping @@ -1016,6 +1051,8 @@ def load_locust_tests(path, dot_env_path=None): project_mapping = load_project_tests(path, dot_env_path) config = { + "variables": project_mapping["debugtalk"]["variables"], + "functions": project_mapping["debugtalk"]["functions"], "refs": project_mapping } tests = [] diff --git a/tests/test_loader.py b/tests/test_loader.py index 5d0a2f08..77a23a1a 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -285,7 +285,7 @@ class TestSuiteLoader(unittest.TestCase): def test_load_teststeps(self): test_block = { - "name": "setup and reset all.", + "name": "setup and reset all (override).", "suite": "setup_and_reset($device_sn)", "output": ["token", "device_sn"] } @@ -322,21 +322,28 @@ class TestSuiteLoader(unittest.TestCase): ) test_block = { "name": "override block", + "times": 3, "variables": [ {"var": 123} ], 'request': { - 'url': '/api/get-token', 'method': 'POST', 'headers': {'user_agent': '$user_agent', 'device_sn': '$device_sn', 'os_platform': '$os_platform', 'app_version': '$app_version'}, 'json': {'sign': '${get_sign($user_agent, $device_sn, $os_platform, $app_version)}'}}, + 'url': '/api/get-token', + 'method': 'POST', + 'headers': {'user_agent': '$user_agent', 'device_sn': '$device_sn', 'os_platform': '$os_platform', 'app_version': '$app_version'}, + 'json': {'sign': '${get_sign($user_agent, $device_sn, $os_platform, $app_version)}'} + }, 'validate': [ {'eq': ['status_code', 201]}, {'len_eq': ['content.token', 32]} ] } - loader._extend_block(test_block, def_block) - self.assertEqual(test_block["name"], "override block") - self.assertIn({'check': 'status_code', 'expect': 201, 'comparator': 'eq'}, test_block["validate"]) - self.assertIn({'check': 'content.token', 'comparator': 'len_eq', 'expect': 32}, test_block["validate"]) + extended_block = loader._extend_block(test_block, def_block) + self.assertEqual(extended_block["name"], "override block") + self.assertIn({'var': 123}, extended_block["variables"]) + self.assertIn({'check': 'status_code', 'expect': 201, 'comparator': 'eq'}, extended_block["validate"]) + self.assertIn({'check': 'content.token', 'comparator': 'len_eq', 'expect': 32}, extended_block["validate"]) + self.assertEqual(extended_block["times"], 3) def test_get_test_definition_api(self): api_def = loader._get_test_definition("get_headers", "def-api", self.project_mapping) @@ -354,7 +361,7 @@ class TestSuiteLoader(unittest.TestCase): with self.assertRaises(exceptions.TestcaseNotFound): loader._get_test_definition("create_and_check_XXX", "def-testcase", self.project_mapping) - def test_merge_validator(self): + def test_extend_validators(self): def_validators = [ {'eq': ['v1', 200]}, {"check": "s2", "expect": 16, "comparator": "len_eq"} @@ -364,21 +371,21 @@ class TestSuiteLoader(unittest.TestCase): {'len_eq': ['s3', 12]} ] - merged_validators = loader._merge_validator(def_validators, current_validators) + extended_validators = loader._extend_validators(def_validators, current_validators) self.assertIn( {"check": "v1", "expect": 201, "comparator": "eq"}, - merged_validators + extended_validators ) self.assertIn( {"check": "s2", "expect": 16, "comparator": "len_eq"}, - merged_validators + extended_validators ) self.assertIn( {"check": "s3", "expect": 12, "comparator": "len_eq"}, - merged_validators + extended_validators ) - def test_merge_validator_with_dict(self): + def test_extend_validators_with_dict(self): def_validators = [ {'eq': ["a", {"v": 1}]}, {'eq': [{"b": 1}, 200]} @@ -388,27 +395,27 @@ class TestSuiteLoader(unittest.TestCase): {'eq': [{"b": 1}, 201]} ] - merged_validators = loader._merge_validator(def_validators, current_validators) - self.assertEqual(len(merged_validators), 3) - self.assertIn({'check': {'b': 1}, 'expect': 201, 'comparator': 'eq'}, merged_validators) - self.assertNotIn({'check': {'b': 1}, 'expect': 200, 'comparator': 'eq'}, merged_validators) + extended_validators = loader._extend_validators(def_validators, current_validators) + self.assertEqual(len(extended_validators), 3) + self.assertIn({'check': {'b': 1}, 'expect': 201, 'comparator': 'eq'}, extended_validators) + self.assertNotIn({'check': {'b': 1}, 'expect': 200, 'comparator': 'eq'}, extended_validators) - def test_merge_extractor(self): - api_extrators = [{"var1": "val1"}, {"var2": "val2"}] - current_extractors = [{"var1": "val111"}, {"var3": "val3"}] + def test_extend_variables(self): + def_variables = [{"var1": "val1"}, {"var2": "val2"}] + ref_variables = [{"var1": "val111"}, {"var3": "val3"}] - merged_extractors = loader._merge_extractor(api_extrators, current_extractors) + extended_variables = loader._extend_variables(def_variables, ref_variables) self.assertIn( {"var1": "val111"}, - merged_extractors + extended_variables ) self.assertIn( {"var2": "val2"}, - merged_extractors + extended_variables ) self.assertIn( {"var3": "val3"}, - merged_extractors + extended_variables ) def test_load_testcases_by_path_files(self):