From e64a606516f61a79a4666946e84ed935e45c64d5 Mon Sep 17 00:00:00 2001 From: jun Date: Thu, 2 Jul 2020 10:51:17 +0800 Subject: [PATCH 1/3] **Add** - New feature: config part support "parameters". example: examples\postman_echo\request_methods\request_with_parameters.yml --- .../request_with_parameters.yml | 34 +++++++ httprunner/make.py | 21 ++++ httprunner/parser.py | 99 +++++++++++++++++++ httprunner/runner.py | 3 +- httprunner/utils.py | 77 +++++++++++++++ 5 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 examples/postman_echo/request_methods/request_with_parameters.yml 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 From 1c23d2093381c4423255cc505f50e03b44225b43 Mon Sep 17 00:00:00 2001 From: jun Date: Thu, 2 Jul 2020 18:11:11 +0800 Subject: [PATCH 2/3] test: #931 add test for parameters --- .../request_with_parameters.yml | 11 ++-- httprunner/make.py | 4 +- httprunner/parser.py | 12 ++--- tests/account.csv | 3 ++ tests/make_test.py | 24 +++++++++ tests/parser_test.py | 27 ++++++++++ tests/utils_test.py | 52 +++++++++++++++++++ 7 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 tests/account.csv diff --git a/examples/postman_echo/request_methods/request_with_parameters.yml b/examples/postman_echo/request_methods/request_with_parameters.yml index fb4788af..38e239c4 100644 --- a/examples/postman_echo/request_methods/request_with_parameters.yml +++ b/examples/postman_echo/request_methods/request_with_parameters.yml @@ -1,12 +1,11 @@ config: - name: "request methods testcase: validate with functions" + name: "request methods testcase: validate with parameters" parameters: user_agent: ["iOS/10.1", "iOS/10.2"] username-password: ${parameterize(request_methods/account.csv)} - app_version: - - ${get_httprunner_version()} + app_version: ${get_app_version()} variables: - foo1: f1 + app_version: f1 base_url: "https://postman-echo.com" verify: False @@ -16,7 +15,7 @@ teststeps: variables: foo1: $username foo2: $password - sum_v: "${sum_two(1, 2)}" + sum_v: "${sum_two(1, $app_version)}" request: method: GET url: /get @@ -30,5 +29,5 @@ teststeps: session_foo2: "body.args.foo2" validate: - eq: ["status_code", 200] - - eq: ["body.args.sum_v", "3"] + - str_eq: ["body.args.sum_v", "${sum_two(1, $app_version)}"] # - 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 3b5c6257..bf3f158d 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -408,7 +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), + "customization_test_start": make_test_start_style(config), "teststeps_chain_style": [ make_teststep_chain_style(step) for step in teststeps ], @@ -610,7 +610,7 @@ def init_make_parser(subparsers): return parser -def make_test_start(config: Dict) -> Text: +def make_test_start_style(config: Dict) -> Text: test_start_style = "" if config["parameters"]: params = config["parameters"] diff --git a/httprunner/parser.py b/httprunner/parser.py index 3c6f79fa..d5178356 100644 --- a/httprunner/parser.py +++ b/httprunner/parser.py @@ -490,13 +490,14 @@ def parse_parameters(parameters, variables_mapping=None, functions_mapping=None) >>> 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}") + + # load project_meta functions + from httprunner.loader import load_project_meta + project_meta = load_project_meta("") + functions_mapping.update(project_meta.functions) parameters = utils.ensure_mapping_format(parameters) for parameter_name, parameter_content in parameters.items(): @@ -520,13 +521,12 @@ def parse_parameters(parameters, variables_mapping=None, functions_mapping=None) 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( + parsed_parameter_content = parse_data( parameter_content, parsed_variables_mapping, functions_mapping diff --git a/tests/account.csv b/tests/account.csv new file mode 100644 index 00000000..c3dae178 --- /dev/null +++ b/tests/account.csv @@ -0,0 +1,3 @@ +username,password +test1,111111 +test2,222222 \ No newline at end of file diff --git a/tests/make_test.py b/tests/make_test.py index f5aeb6ae..e8e8fb15 100644 --- a/tests/make_test.py +++ b/tests/make_test.py @@ -9,6 +9,7 @@ from httprunner.make import ( make_teststep_chain_style, pytest_files_run_set, ensure_file_abs_path_valid, + make_test_start_style ) from httprunner import loader @@ -214,3 +215,26 @@ from request_methods.request_with_functions_test import ( teststep_chain_style, """Step(RunRequest("get with params").with_variables(**{'foo1': 'bar1', 'foo2': 123, 'sum_v': '${sum_two(1, 2)}'}).get("/get").with_params(**{'foo1': '$foo1', 'foo2': '$foo2', 'sum_v': '$sum_v'}).with_headers(**{'User-Agent': 'HttpRunner/${get_httprunner_version()}'}).extract().with_jmespath('body.args.foo1', 'session_foo1').with_jmespath('body.args.foo2', 'session_foo2').validate().assert_equal("status_code", 200).assert_equal("body.args.sum_v", "3"))""", ) + + def test_make_test_start_style(self): + params = { + "user_agent": ["iOS/10.1", "iOS/10.2"], + "username-password": "${parameterize(request_methods/account.csv)}", + "app_version": "${get_app_version()}", + } + config = { + "parameters": params + } + self.assertEqual( + make_test_start_style(config), + 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) + """ + ) \ No newline at end of file diff --git a/tests/parser_test.py b/tests/parser_test.py index e7ac19b4..7026711b 100644 --- a/tests/parser_test.py +++ b/tests/parser_test.py @@ -452,3 +452,30 @@ class TestParserBasic(unittest.TestCase): self.assertEqual(parsed_testcase["headers"]["random"], variables["random"]) self.assertEqual(parsed_testcase["body"], variables["data"]) self.assertEqual(parsed_testcase["headers"]["sum"], 3) + + def test_parse_parameters_testcase(self): + variables = { + "user_agent": "chrome", + "sum": 5, + } + param = [ + { + "user_agent": ["iOS/10.1", "iOS/10.2"], + "username-password": "${parameterize(account.csv)}", + "sum": "${add_two_nums(1, 2)}", + } + ] + + functions = { + "add_two_nums": lambda a, b=1: [a + b, b-a], + } + + parsed_params = parser.parse_parameters(param, variables, functions) + self.assertIn({'username': 'test1', 'password': '111111', 'user_agent': 'iOS/10.1', 'sum': 3}, parsed_params) + self.assertIn({'username': 'test1', 'password': '111111', 'user_agent': 'iOS/10.1', 'sum': 1}, parsed_params) + self.assertIn({'username': 'test1', 'password': '111111', 'user_agent': 'iOS/10.2', 'sum': 3}, parsed_params) + self.assertIn({'username': 'test1', 'password': '111111', 'user_agent': 'iOS/10.2', 'sum': 1}, parsed_params) + self.assertIn({'username': 'test2', 'password': '222222', 'user_agent': 'iOS/10.1', 'sum': 3}, parsed_params) + self.assertIn({'username': 'test2', 'password': '222222', 'user_agent': 'iOS/10.1', 'sum': 1}, parsed_params) + self.assertIn({'username': 'test2', 'password': '222222', 'user_agent': 'iOS/10.2', 'sum': 3}, parsed_params) + self.assertIn({'username': 'test2', 'password': '222222', 'user_agent': 'iOS/10.2', 'sum': 1}, parsed_params) diff --git a/tests/utils_test.py b/tests/utils_test.py index 99771c25..2d767e05 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -125,3 +125,55 @@ class TestUtils(unittest.TestCase): override_config_variables(step_variables, config_variables), {"base_url": "https://httpbin.org", "foo1": "bar1"}, ) + + def test_ensure_mapping_format(self): + map_list = [ + {"a": 1}, + {"b": 2} + ] + ordered_dict = utils.ensure_mapping_format(map_list) + self.assertIsInstance(ordered_dict, dict) + self.assertIn("a", ordered_dict) + + def test_cartesian_product_one(self): + parameters_content_list = [ + [ + {"a": 1}, + {"a": 2} + ] + ] + product_list = utils.gen_cartesian_product(*parameters_content_list) + self.assertEqual( + product_list, + [ + {"a": 1}, + {"a": 2} + ] + ) + + def test_cartesian_product_multiple(self): + parameters_content_list = [ + [ + {"a": 1}, + {"a": 2} + ], + [ + {"x": 111, "y": 112}, + {"x": 121, "y": 122} + ] + ] + product_list = utils.gen_cartesian_product(*parameters_content_list) + self.assertEqual( + product_list, + [ + {'a': 1, 'x': 111, 'y': 112}, + {'a': 1, 'x': 121, 'y': 122}, + {'a': 2, 'x': 111, 'y': 112}, + {'a': 2, 'x': 121, 'y': 122} + ] + ) + + def test_cartesian_product_empty(self): + parameters_content_list = [] + product_list = utils.gen_cartesian_product(*parameters_content_list) + self.assertEqual(product_list, []) \ No newline at end of file From 4595022f62862cbfb2852bbb0aa7ffc3a1b6d615 Mon Sep 17 00:00:00 2001 From: jun Date: Fri, 3 Jul 2020 10:23:10 +0800 Subject: [PATCH 3/3] fixed: #931 unit test parameters --- examples/postman_echo/debugtalk.py | 5 ++++- {tests => examples/postman_echo/request_methods}/account.csv | 3 ++- httprunner/make.py | 2 +- httprunner/models.py | 1 + httprunner/runner.py | 3 ++- tests/parser_test.py | 2 +- 6 files changed, 11 insertions(+), 5 deletions(-) rename {tests => examples/postman_echo/request_methods}/account.csv (55%) diff --git a/examples/postman_echo/debugtalk.py b/examples/postman_echo/debugtalk.py index a574ab2a..d28b829b 100644 --- a/examples/postman_echo/debugtalk.py +++ b/examples/postman_echo/debugtalk.py @@ -4,7 +4,6 @@ from httprunner import __version__ def get_httprunner_version(): return __version__ - def sum_two(m, n): return m + n @@ -15,3 +14,7 @@ def get_testcase_config_variables(): def get_testsuite_config_variables(): return {"foo1": "testsuite_config_bar1", "foo2": "testsuite_config_bar2"} + + +def get_app_version(): + return [3.1, 3.0] diff --git a/tests/account.csv b/examples/postman_echo/request_methods/account.csv similarity index 55% rename from tests/account.csv rename to examples/postman_echo/request_methods/account.csv index c3dae178..67ce22c6 100644 --- a/tests/account.csv +++ b/examples/postman_echo/request_methods/account.csv @@ -1,3 +1,4 @@ username,password test1,111111 -test2,222222 \ No newline at end of file +test2,222222 +test3,333333 \ No newline at end of file diff --git a/httprunner/make.py b/httprunner/make.py index bf3f158d..ada16813 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -612,7 +612,7 @@ def init_make_parser(subparsers): def make_test_start_style(config: Dict) -> Text: test_start_style = "" - if config["parameters"]: + if "parameters" in config.keys(): params = config["parameters"] test_start_style = f""" import pytest diff --git a/httprunner/models.py b/httprunner/models.py index 51264c33..04fbab23 100644 --- a/httprunner/models.py +++ b/httprunner/models.py @@ -37,6 +37,7 @@ class TConfig(BaseModel): base_url: BaseUrl = "" # Text: prepare variables in debugtalk.py, ${gen_variables()} variables: Union[VariablesMapping, Text] = {} + parameters: Union[VariablesMapping, Text] = {} # setup_hooks: Hooks = [] # teardown_hooks: Hooks = [] export: Export = [] diff --git a/httprunner/runner.py b/httprunner/runner.py index ebdc7607..d2129f03 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -435,7 +435,8 @@ class HttpRunner(object): # parse config name config_variables = self.__config.variables - config_variables.update(parametrize) + if parametrize: + 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/tests/parser_test.py b/tests/parser_test.py index 7026711b..96d3cd20 100644 --- a/tests/parser_test.py +++ b/tests/parser_test.py @@ -461,7 +461,7 @@ class TestParserBasic(unittest.TestCase): param = [ { "user_agent": ["iOS/10.1", "iOS/10.2"], - "username-password": "${parameterize(account.csv)}", + "username-password": "${parameterize(request_methods/account.csv)}", "sum": "${add_two_nums(1, 2)}", } ]