diff --git a/httprunner/__about__.py b/httprunner/__about__.py index 8f896fe1..00c0029c 100644 --- a/httprunner/__about__.py +++ b/httprunner/__about__.py @@ -1,7 +1,7 @@ __title__ = 'HttpRunner' __description__ = 'HTTP test runner, not just about api test and load test.' __url__ = 'https://github.com/HttpRunner/HttpRunner' -__version__ = '0.9.9' +__version__ = '1.0.0' __author__ = 'debugtalk' __author_email__ = 'mail@debugtalk.com' __license__ = 'MIT' diff --git a/httprunner/task.py b/httprunner/task.py index cec4abd3..f2ffbc53 100644 --- a/httprunner/task.py +++ b/httprunner/task.py @@ -59,7 +59,7 @@ class TestSuite(unittest.TestSuite): self.config_dict["variables"] = utils.override_variables_binds(variables, variables_mapping) parameters = self.config_dict.get("parameters", []) - cartesian_product_parameters = testcase.gen_cartesian_product_parameters( + cartesian_product_parameters = testcase.parse_parameters( parameters, self.config_dict["path"] ) or [{}] diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 503bd046..d80706cb 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -11,8 +11,8 @@ import yaml from httprunner import exception, logger, utils variable_regexp = r"\$([\w_]+)" -function_regexp = r"\$\{([\w_]+\([\$\w_ =,]*\))\}" -function_regexp_compile = re.compile(r"^([\w_]+)\(([\$\w_ =,]*)\)$") +function_regexp = r"\$\{([\w_]+\([\$\w\.\-_ =,]*\))\}" +function_regexp_compile = re.compile(r"^([\w_]+)\(([\$\w\.\-_ =,]*)\)$") test_def_overall_dict = { "loaded": False, "api": {}, @@ -73,10 +73,6 @@ def _load_csv_file(csv_file): if not parameter_list: # first line will always be parameter name - expected_filename = "{}.csv".format("-".join(line_data)) - if not csv_file.endswith(expected_filename): - raise exception.FileFormatError("CSV file name does not match with headers: {}".format(csv_file)) - parameter_list = line_data collums_num = len(parameter_list) continue @@ -642,34 +638,54 @@ def gen_cartesian_product(*args): return product_list -def gen_cartesian_product_parameters(parameters, testset_path): +def parse_parameters(parameters, testset_path=None): """ parse parameters and generate cartesian product @params - (list) parameters: parameter name and fetch method + (list) parameters: parameter name and value in list + parameter value may be in three types: + (1) data list + (2) call built-in parameterize function + (3) call custom function in debugtalk.py e.g. [ - {"user_agent": "Random"}, - {"app_version": "Sequential"} + {"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]}, + {"username-password": "${parameterize(account.csv)}"}, + {"app_version": "${gen_app_version()}"} ] - (str) testset_path: testset file path, used for locating csv file + (str) testset_path: testset file path, used for locating csv file and debugtalk.py @return cartesian product in list """ - parameters_content_list = [] + testcase_parser = TestcaseParser(file_path=testset_path) + + parsed_parameters_list = [] for parameter in parameters: - parameter_name, fetch_method = list(parameter.items())[0] - parameter_file_path = os.path.join( - os.path.dirname(testset_path), - "{}.csv".format(parameter_name) - ) - csv_content_list = load_file(parameter_file_path) + parameter_name, parameter_content = list(parameter.items())[0] + parameter_name_list = parameter_name.split("-") - if fetch_method.lower() == "random": - random.shuffle(csv_content_list) + if isinstance(parameter_content, list): + # (1) data list + # e.g. {"app_version": ["2.8.5", "2.8.6"]} + # => [{"app_version": "2.8.5", "app_version": "2.8.6"}] + # e.g. {"username-password": [["user1", "111111"], ["test2", "222222"]} + # => [{"username": "user1", "password": "111111"}, {"username": "user2", "password": "222222"}] + parameter_content_list = [ + dict(zip(parameter_name_list, [parameter_item])) + for parameter_item in parameter_content + ] + else: + # (2) & (3) + parsed_parameter_content = testcase_parser.eval_content_with_bindings(parameter_content) + # e.g. [{'app_version': '2.8.5'}, {'app_version': '2.8.6'}] + # e.g. [{"username": "user1", "password": "111111"}, {"username": "user2", "password": "222222"}] + parameter_content_list = [ + # get subset by parameter name + {key: parameter_item[key] for key in parameter_name_list} + for parameter_item in parsed_parameter_content + ] - parameters_content_list.append(csv_content_list) - - return gen_cartesian_product(*parameters_content_list) + parsed_parameters_list.append(parameter_content_list) + return gen_cartesian_product(*parsed_parameters_list) class TestcaseParser(object): @@ -726,19 +742,34 @@ class TestcaseParser(object): raise exception.ParamsError( "{} is not defined in bind {}s!".format(item_name, item_type)) + def parameterize(self, csv_file_name, fetch_method="Sequential"): + parameter_file_path = os.path.join( + os.path.dirname(self.file_path), + "{}".format(csv_file_name) + ) + csv_content_list = load_file(parameter_file_path) + + if fetch_method.lower() == "random": + random.shuffle(csv_content_list) + + return csv_content_list + def _eval_content_functions(self, content): functions_list = extract_functions(content) for func_content in functions_list: function_meta = parse_function(func_content) func_name = function_meta['func_name'] - func = self.get_bind_item("function", func_name) - args = function_meta.get('args', []) kwargs = function_meta.get('kwargs', {}) args = self.eval_content_with_bindings(args) kwargs = self.eval_content_with_bindings(kwargs) - eval_value = func(*args, **kwargs) + + if func_name in ["parameterize", "P"]: + eval_value = self.parameterize(*args, **kwargs) + else: + func = self.get_bind_item("function", func_name) + eval_value = func(*args, **kwargs) func_content = "${" + func_content + "}" if func_content == content: diff --git a/tests/data/username-password.csv b/tests/data/account.csv similarity index 100% rename from tests/data/username-password.csv rename to tests/data/account.csv diff --git a/tests/data/debugtalk.py b/tests/data/debugtalk.py index 808e0d2f..68049a81 100644 --- a/tests/data/debugtalk.py +++ b/tests/data/debugtalk.py @@ -48,3 +48,15 @@ def skip_test_in_production_env(): """ skip this test in production environment """ return os.environ["TEST_ENV"] == "PRODUCTION" + +def gen_app_version(): + return [ + {"app_version": "2.8.5"}, + {"app_version": "2.8.6"} + ] + +def get_account(): + return [ + {"username": "user1", "password": "111111"}, + {"username": "user2", "password": "222222"} + ] diff --git a/tests/data/demo_parameters.yml b/tests/data/demo_parameters.yml index df4a06a9..bc08e408 100644 --- a/tests/data/demo_parameters.yml +++ b/tests/data/demo_parameters.yml @@ -1,13 +1,13 @@ - config: name: "user management testset." parameters: - - user_agent: Random - - app_version: Sequential + - user_agent: ["iOS/10.1", "iOS/10.2", "iOS/10.3"] + - app_version: ${gen_app_version()} + - username: ${parameterize(account.csv)} variables: - user_agent: 'iOS/10.3' - device_sn: ${gen_random_string(15)} - os_platform: 'ios' - - app_version: '2.8.6' request: base_url: $BASE_URL headers: @@ -17,7 +17,7 @@ - token - test: - name: get token with $user_agent and $app_version + name: get token with $user_agent and $app_version, username $username api: get_token($user_agent, $device_sn, $os_platform, $app_version) extract: - token: content.token diff --git a/tests/test_runner.py b/tests/test_runner.py index 8fa59243..4079aac6 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -164,4 +164,4 @@ class TestRunner(ApiServerUnittest): result = HttpRunner(testcase_file_path).run() self.assertTrue(result["success"]) self.assertIn("token", result["output"]) - self.assertEqual(result["stat"]["testsRun"], 6) + self.assertEqual(result["stat"]["testsRun"], 3 * 2 * 3) diff --git a/tests/test_testcase.py b/tests/test_testcase.py index f9b7453c..db324aad 100644 --- a/tests/test_testcase.py +++ b/tests/test_testcase.py @@ -51,7 +51,7 @@ class TestcaseParserUnittest(unittest.TestCase): def test_load_csv_file_multiple_parameters(self): csv_file_path = os.path.join( - os.getcwd(), 'tests/data/username-password.csv') + os.getcwd(), 'tests/data/account.csv') csv_content = testcase.load_file(csv_file_path) self.assertEqual( csv_content, @@ -105,40 +105,70 @@ class TestcaseParserUnittest(unittest.TestCase): product_list = testcase.gen_cartesian_product(*parameters_content_list) self.assertEqual(product_list, []) - def test_gen_cartesian_product_parameters_one_to_one(self): + def test_parse_parameters_raw_list(self): parameters = [ - {"user_agent": "random"}, - {"app_version": "sequential"} + {"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]}, + {"username-password": [("user1", "111111"), ("test2", "222222")]} ] - testset_path = os.path.join( - os.getcwd(), - "tests/data/demo_parameters.yml" - ) - cartesian_product_parameters = testcase.gen_cartesian_product_parameters( - parameters, - testset_path - ) + cartesian_product_parameters = testcase.parse_parameters(parameters) self.assertEqual( len(cartesian_product_parameters), - 6 + 3 * 2 ) - def test_gen_cartesian_product_parameters_one_to_multiple(self): + def test_parse_parameters_parameterize(self): parameters = [ - {"user_agent": "random"}, - {"username-password": "sequential"} + {"app_version": "${parameterize(app_version.csv)}"}, + {"username-password": "${parameterize(account.csv)}"} ] testset_path = os.path.join( os.getcwd(), "tests/data/demo_parameters.yml" ) - cartesian_product_parameters = testcase.gen_cartesian_product_parameters( + cartesian_product_parameters = testcase.parse_parameters( parameters, testset_path ) self.assertEqual( len(cartesian_product_parameters), - 9 + 2 * 3 + ) + + def test_parse_parameters_custom_function(self): + parameters = [ + {"app_version": "${gen_app_version()}"}, + {"username-password": "${get_account()}"} + ] + testset_path = os.path.join( + os.getcwd(), + "tests/data/demo_parameters.yml" + ) + cartesian_product_parameters = testcase.parse_parameters( + parameters, + testset_path + ) + self.assertEqual( + len(cartesian_product_parameters), + 2 * 2 + ) + + def test_parse_parameters_mix(self): + parameters = [ + {"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]}, + {"app_version": "${gen_app_version()}"}, + {"username-password": "${parameterize(account.csv)}"} + ] + testset_path = os.path.join( + os.getcwd(), + "tests/data/demo_parameters.yml" + ) + cartesian_product_parameters = testcase.parse_parameters( + parameters, + testset_path + ) + self.assertEqual( + len(cartesian_product_parameters), + 3 * 2 * 3 ) def test_load_yaml_file_file_format_error(self):