diff --git a/examples/postman_echo/request_methods/request_with_parameters.yml b/examples/postman_echo/request_methods/request_with_parameters.yml new file mode 100644 index 00000000..fb4788af --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_parameters.yml @@ -0,0 +1,34 @@ +config: + name: "request methods testcase: validate with functions" + parameters: + user_agent: ["iOS/10.1", "iOS/10.2"] + username-password: ${parameterize(request_methods/account.csv)} + app_version: + - ${get_httprunner_version()} + variables: + foo1: f1 + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: get with params + variables: + foo1: $username + foo2: $password + sum_v: "${sum_two(1, 2)}" + request: + method: GET + url: /get + params: + foo1: $foo1 + foo2: $foo2 + sum_v: $sum_v + headers: + User-Agent: $user_agent,$app_version + extract: + session_foo2: "body.args.foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.args.sum_v", "3"] +# - less_than: ["body.args.sum_v", "${sum_two(2, 2)}"] FIXME: TypeError: '<' not supported between instances of 'str' and 'int' diff --git a/httprunner/make.py b/httprunner/make.py index c0bf71c2..3b5c6257 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -53,6 +53,7 @@ from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase {% endfor %} class {{ class_name }}(HttpRunner): + {{ customization_test_start }} config = {{ config_chain_style }} teststeps = [ @@ -407,6 +408,7 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text: "class_name": f"TestCase{testcase_cls_name}", "imports_list": imports_list, "config_chain_style": make_config_chain_style(config), + "customization_test_start": make_test_start(config), "teststeps_chain_style": [ make_teststep_chain_style(step) for step in teststeps ], @@ -606,3 +608,22 @@ def init_make_parser(subparsers): ) return parser + + +def make_test_start(config: Dict) -> Text: + test_start_style = "" + if config["parameters"]: + params = config["parameters"] + test_start_style = f""" + import pytest + from httprunner.parser import parse_parameters + + param = [{params}] + + @pytest.mark.parametrize('parametrize', parse_parameters(param)) + def test_start(self, parametrize): + super().test_start(parametrize) + """ + else: + pass + return test_start_style \ No newline at end of file diff --git a/httprunner/parser.py b/httprunner/parser.py index 32a17192..3c6f79fa 100644 --- a/httprunner/parser.py +++ b/httprunner/parser.py @@ -463,3 +463,102 @@ def parse_variables_mapping( parsed_variables[var_name] = parsed_value return parsed_variables + + +def parse_parameters(parameters, variables_mapping=None, functions_mapping=None): + """ parse parameters and generate cartesian product. + + Args: + parameters (list) parameters: parameter name and value in list + parameter value may be in three types: + (1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"] + (2) call built-in parameterize function, "${parameterize(account.csv)}" + (3) call custom function in debugtalk.py, "${gen_app_version()}" + + variables_mapping (dict): variables mapping loaded from testcase config + functions_mapping (dict): functions mapping loaded from debugtalk.py + + Returns: + list: cartesian product list + + Examples: + >>> parameters = [ + {"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]}, + {"username-password": "${parameterize(account.csv)}"}, + {"app_version": "${gen_app_version()}"} + ] + >>> parse_parameters(parameters) + + """ + from httprunner.loader import load_project_meta + variables_mapping = variables_mapping or {} + functions_mapping = functions_mapping or {} + parsed_parameters_list = [] + # project_meta = load_project_meta("") + # functions_mapping.update(project_meta.functions) + # logger.warning(f"functions_mapping: {functions_mapping}") + + parameters = utils.ensure_mapping_format(parameters) + for parameter_name, parameter_content in parameters.items(): + parameter_name_list = parameter_name.split("-") + + 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 = [] + for parameter_item in parameter_content: + if not isinstance(parameter_item, (list, tuple)): + # "2.8.5" => ["2.8.5"] + parameter_item = [parameter_item] + + # ["app_version"], ["2.8.5"] => {"app_version": "2.8.5"} + # ["username", "password"], ["user1", "111111"] => {"username": "user1", "password": "111111"} + parameter_content_dict = dict(zip(parameter_name_list, parameter_item)) + + parameter_content_list.append(parameter_content_dict) + else: + pass + # (2) & (3) + parsed_variables_mapping = parse_variables_mapping( + variables_mapping, + functions_mapping + ) + parsed_parameter_content = parse_string( + parameter_content, + parsed_variables_mapping, + functions_mapping + ) + if not isinstance(parsed_parameter_content, list): + raise exceptions.ParamsError(f"{parsed_parameter_content} parameters syntax error!") + + parameter_content_list = [] + for parameter_item in parsed_parameter_content: + if isinstance(parameter_item, dict): + # get subset by parameter name + # {"app_version": "${gen_app_version()}"} + # gen_app_version() => [{'app_version': '2.8.5'}, {'app_version': '2.8.6'}] + # {"username-password": "${get_account()}"} + # get_account() => [ + # {"username": "user1", "password": "111111"}, + # {"username": "user2", "password": "222222"} + # ] + parameter_dict = {key: parameter_item[key] for key in parameter_name_list} + # elif isinstance(parameter_item, (list, tuple)): + # # {"username-password": "${get_account()}"} + # # get_account() => [("user1", "111111"), ("user2", "222222")] + # parameter_dict = dict(zip(parameter_name_list, parameter_item)) + elif len(parameter_name_list) == 1: + # {"user_agent": "${get_user_agent()}"} + # get_user_agent() => ["iOS/10.1", "iOS/10.2"] + parameter_dict = { + parameter_name_list[0]: parameter_item + } + + parameter_content_list.append(parameter_dict) + + parsed_parameters_list.append(parameter_content_list) + + return utils.gen_cartesian_product(*parsed_parameters_list) diff --git a/httprunner/runner.py b/httprunner/runner.py index f916a2d7..ebdc7607 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -421,7 +421,7 @@ class HttpRunner(object): step_datas=self.__step_datas, ) - def test_start(self) -> "HttpRunner": + def test_start(self, parametrize=None) -> "HttpRunner": """main entrance, discovered by pytest""" self.__init_tests__() self.__project_meta = self.__project_meta or load_project_meta( @@ -435,6 +435,7 @@ class HttpRunner(object): # parse config name config_variables = self.__config.variables + config_variables.update(parametrize) config_variables.update(self.__session_variables) self.__config.name = parse_data( self.__config.name, config_variables, self.__project_meta.functions diff --git a/httprunner/utils.py b/httprunner/utils.py index c0e27ed9..597c728c 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -5,6 +5,7 @@ import os.path import platform import uuid from multiprocessing import Queue +import itertools from typing import Dict, List, Any import sentry_sdk @@ -220,3 +221,79 @@ def is_support_multiprocessing() -> bool: except (ImportError, OSError): # system that does not support semaphores(dependency of multiprocessing), like Android termux return False + + +def ensure_mapping_format(variables): + """ ensure variables are in mapping format. + + Args: + variables (list/dict): original variables + + Returns: + dict: ensured variables in dict format + + Examples: + >>> variables = [ + {"a": 1}, + {"b": 2} + ] + >>> print(ensure_mapping_format(variables)) + { + "a": 1, + "b": 2 + } + + """ + if isinstance(variables, list): + variables_dict = {} + for map_dict in variables: + variables_dict.update(map_dict) + + return variables_dict + + elif isinstance(variables, dict): + return variables + + else: + raise exceptions.ParamsError("variables format error!") + + +def gen_cartesian_product(*args): + """ generate cartesian product for lists + + Args: + args (list of list): lists to be generated with cartesian product + + Returns: + list: cartesian product in list + + Examples: + + >>> arg1 = [{"a": 1}, {"a": 2}] + >>> arg2 = [{"x": 111, "y": 112}, {"x": 121, "y": 122}] + >>> args = [arg1, arg2] + >>> gen_cartesian_product(*args) + >>> # same as below + >>> gen_cartesian_product(arg1, arg2) + [ + {'a': 1, 'x': 111, 'y': 112}, + {'a': 1, 'x': 121, 'y': 122}, + {'a': 2, 'x': 111, 'y': 112}, + {'a': 2, 'x': 121, 'y': 122} + ] + + """ + if not args: + return [] + elif len(args) == 1: + return args[0] + + product_list = [] + for product_item_tuple in itertools.product(*args): + product_item_dict = {} + for item in product_item_tuple: + product_item_dict.update(item) + + product_list.append(product_item_dict) + + return product_list