diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index bc5b0ed4..79f3f676 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -3,7 +3,7 @@ name: integration_test on: push: branches: - - master + - dev pull_request: branches: - master diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index dddc5860..d7454f60 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -3,7 +3,7 @@ name: unittest on: push: branches: - - master + - dev pull_request: branches: - master diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5bd0fc5e..9b7327b8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,17 @@ # Release History +## 3.1.3 (2020-07-06) + +**Added** + +- feat: implement `parameters` feature + +**Fixed** + +- fix: validate with variable or function whose evaluation result is "" or not text +- fix: raise TestCaseFormatError if teststep validate invalid +- fix: raise TestCaseFormatError if ref testcase is invalid + ## 3.1.2 (2020-06-29) **Fixed** diff --git a/examples/httpbin/basic_test.py b/examples/httpbin/basic_test.py index f541798f..78a39bef 100644 --- a/examples/httpbin/basic_test.py +++ b/examples/httpbin/basic_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: basic.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseBasic(HttpRunner): + config = Config("basic test with httpbin").base_url("https://httpbin.org/") teststeps = [ diff --git a/examples/httpbin/hooks_test.py b/examples/httpbin/hooks_test.py index 86a1b0d7..6ec448e9 100644 --- a/examples/httpbin/hooks_test.py +++ b/examples/httpbin/hooks_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: hooks.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseHooks(HttpRunner): + config = Config("basic test with httpbin").base_url("${get_httpbin_server()}") teststeps = [ diff --git a/examples/httpbin/load_image_test.py b/examples/httpbin/load_image_test.py index 47b05af1..f2785770 100644 --- a/examples/httpbin/load_image_test.py +++ b/examples/httpbin/load_image_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: load_image.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseLoadImage(HttpRunner): + config = Config("load images").base_url("${get_httpbin_server()}") teststeps = [ diff --git a/examples/httpbin/upload_test.py b/examples/httpbin/upload_test.py index cdec89fd..0d4ccd89 100644 --- a/examples/httpbin/upload_test.py +++ b/examples/httpbin/upload_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: upload.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseUpload(HttpRunner): + config = Config("test upload file with httpbin").base_url("${get_httpbin_server()}") teststeps = [ diff --git a/examples/httpbin/validate_test.py b/examples/httpbin/validate_test.py index ed15c14d..9de43c70 100644 --- a/examples/httpbin/validate_test.py +++ b/examples/httpbin/validate_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: validate.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseValidate(HttpRunner): + config = Config("basic test with httpbin").base_url("http://httpbin.org/") teststeps = [ diff --git a/examples/postman_echo/debugtalk.py b/examples/postman_echo/debugtalk.py index d28b829b..c0b2aebb 100644 --- a/examples/postman_echo/debugtalk.py +++ b/examples/postman_echo/debugtalk.py @@ -4,6 +4,7 @@ from httprunner import __version__ def get_httprunner_version(): return __version__ + def sum_two(m, n): return m + n @@ -18,3 +19,7 @@ def get_testsuite_config_variables(): def get_app_version(): return [3.1, 3.0] + + +def calculate_two_nums(a, b=1): + return [a + b, b - a] diff --git a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py index 50eb0cc8..df68aa1c 100644 --- a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: request_methods/request_with_functions.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseRequestWithFunctions(HttpRunner): + config = ( Config("request with functions") .variables( diff --git a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py index 2d131814..99b6e3a3 100644 --- a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py @@ -1,11 +1,13 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: request_methods/request_with_testcase_reference.yml + import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from request_methods.request_with_functions_test import ( @@ -14,6 +16,7 @@ from request_methods.request_with_functions_test import ( class TestCaseRequestWithTestcaseReference(HttpRunner): + config = ( Config("request with referenced testcase") .variables( diff --git a/examples/postman_echo/request_methods/hardcode_test.py b/examples/postman_echo/request_methods/hardcode_test.py index 70e5e3c9..324d5202 100644 --- a/examples/postman_echo/request_methods/hardcode_test.py +++ b/examples/postman_echo/request_methods/hardcode_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: request_methods/hardcode.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseHardcode(HttpRunner): + config = ( Config("request methods testcase in hardcode") .base_url("https://postman-echo.com") diff --git a/examples/postman_echo/request_methods/request_with_functions_test.py b/examples/postman_echo/request_methods/request_with_functions_test.py index 996eed64..4bbeeb6e 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: request_methods/request_with_functions.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseRequestWithFunctions(HttpRunner): + config = ( Config("request methods testcase with functions") .variables( diff --git a/examples/postman_echo/request_methods/request_with_parameters_test.py b/examples/postman_echo/request_methods/request_with_parameters_test.py new file mode 100644 index 00000000..5f90c15d --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_parameters_test.py @@ -0,0 +1,56 @@ +# NOTE: Generated By HttpRunner v3.1.3 +# FROM: request_methods/request_with_parameters.yml + + +import pytest +from httprunner import Parameters + + +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase + + +class TestCaseRequestWithParameters(HttpRunner): + @pytest.mark.parametrize( + "param", + Parameters( + { + "user_agent": ["iOS/10.1", "iOS/10.2"], + "username-password": "${parameterize(request_methods/account.csv)}", + "app_version": "${get_app_version()}", + } + ), + ) + def test_start(self, param): + super().test_start(param) + + config = ( + Config("request methods testcase: validate with parameters") + .variables(**{"app_version": "f1"}) + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunRequest("get with params") + .with_variables( + **{ + "foo1": "$username", + "foo2": "$password", + "sum_v": "${sum_two(1, $app_version)}", + } + ) + .get("/get") + .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}) + .with_headers(**{"User-Agent": "$user_agent,$app_version"}) + .extract() + .with_jmespath("body.args.foo2", "session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_string_equals("body.args.sum_v", "${sum_two(1, $app_version)}") + ), + ] + + +if __name__ == "__main__": + TestCaseRequestWithParameters().test_start() diff --git a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py index 2d57795e..e005713b 100644 --- a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py @@ -1,11 +1,13 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: request_methods/request_with_testcase_reference.yml + import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from request_methods.request_with_functions_test import ( @@ -14,6 +16,7 @@ from request_methods.request_with_functions_test import ( class TestCaseRequestWithTestcaseReference(HttpRunner): + config = ( Config("request methods testcase: reference testcase") .variables( diff --git a/examples/postman_echo/request_methods/request_with_variables_test.py b/examples/postman_echo/request_methods/request_with_variables_test.py index cacc4621..ebaefe52 100644 --- a/examples/postman_echo/request_methods/request_with_variables_test.py +++ b/examples/postman_echo/request_methods/request_with_variables_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: request_methods/request_with_variables.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseRequestWithVariables(HttpRunner): + config = ( Config("request methods testcase with variables") .variables(**{"foo1": "testcase_config_bar1", "foo2": "testcase_config_bar2"}) diff --git a/examples/postman_echo/request_methods/validate_with_functions_test.py b/examples/postman_echo/request_methods/validate_with_functions_test.py index 4b67bba5..1a6ca71d 100644 --- a/examples/postman_echo/request_methods/validate_with_functions_test.py +++ b/examples/postman_echo/request_methods/validate_with_functions_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: request_methods/validate_with_functions.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseValidateWithFunctions(HttpRunner): + config = ( Config("request methods testcase: validate with functions") .variables(**{"foo1": "session_bar1"}) diff --git a/examples/postman_echo/request_methods/validate_with_variables_test.py b/examples/postman_echo/request_methods/validate_with_variables_test.py index 07f6d5b3..6045dc47 100644 --- a/examples/postman_echo/request_methods/validate_with_variables_test.py +++ b/examples/postman_echo/request_methods/validate_with_variables_test.py @@ -1,10 +1,12 @@ -# NOTE: Generated By HttpRunner v3.1.2 +# NOTE: Generated By HttpRunner v3.1.3 # FROM: request_methods/validate_with_variables.yml + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseValidateWithVariables(HttpRunner): + config = ( Config("request methods testcase: validate with variables") .variables(**{"foo1": "session_bar1"}) diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 6118a14a..2fd95302 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,8 +1,9 @@ -__version__ = "3.1.2" +__version__ = "3.1.3" __description__ = "One-stop solution for HTTP(S) testing." # import firstly for monkey patch if needed from httprunner.ext.locust import main_locusts +from httprunner.parser import parse_parameters as Parameters from httprunner.runner import HttpRunner from httprunner.testcase import Config, Step, RunRequest, RunTestCase @@ -14,4 +15,5 @@ __all__ = [ "Step", "RunRequest", "RunTestCase", + "Parameters", ] diff --git a/httprunner/compat.py b/httprunner/compat.py index 21f7a809..dd6b526e 100644 --- a/httprunner/compat.py +++ b/httprunner/compat.py @@ -182,6 +182,10 @@ def _ensure_step_attachment(step: Dict) -> Dict: test_dict["export"] = step["export"] if "validate" in step: + if not isinstance(step["validate"], List): + raise exceptions.TestCaseFormatError( + f'Invalid teststep validate: {step["validate"]}' + ) test_dict["validate"] = _convert_validators(step["validate"]) if "validate_script" in step: diff --git a/httprunner/ext/har2case/core.py b/httprunner/ext/har2case/core.py index 7535270b..31796862 100644 --- a/httprunner/ext/har2case/core.py +++ b/httprunner/ext/har2case/core.py @@ -251,7 +251,12 @@ class HarParser(object): encoding = resp_content_dict.get("encoding") if encoding and encoding == "base64": - content = base64.b64decode(text).decode("utf-8") + content = base64.b64decode(text) + try: + content = content.decode("utf-8") + except UnicodeDecodeError: + logger.warning(f"failed to decode base64 content with utf-8 !") + return else: content = text diff --git a/httprunner/make.py b/httprunner/make.py index ada16813..13b95460 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -37,23 +37,31 @@ pytest_files_run_set: Set = set() __TEMPLATE__ = jinja2.Template( """# NOTE: Generated By HttpRunner v{{ version }} # FROM: {{ testcase_path }} + {% if imports_list and diff_levels > 0 %} import sys from pathlib import Path - -sys.path.insert(0, str(Path(__file__) -{% for _ in range(diff_levels) %} -.parent -{% endfor %} -)) +sys.path.insert(0, str(Path(__file__){% for _ in range(diff_levels) %}.parent{% endfor %})) {% endif %} + +{% if parameters %} +import pytest +from httprunner import Parameters +{% endif %} + from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase {% for import_str in imports_list %} {{ import_str }} {% endfor %} class {{ class_name }}(HttpRunner): - {{ customization_test_start }} + + {% if parameters %} + @pytest.mark.parametrize("param", Parameters({{parameters}})) + def test_start(self, param): + super().test_start(param) + {% endif %} + config = {{ config_chain_style }} teststeps = [ @@ -367,6 +375,9 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text: ref_testcase_path = __ensure_absolute(teststep["testcase"]) test_content = load_test_file(ref_testcase_path) + if not isinstance(test_content, Dict): + raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}") + # api in v2 format, convert to v3 testcase if "request" in test_content and "name" in test_content: test_content = ensure_testcase_v3_api(test_content) @@ -408,7 +419,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_style(config), + "parameters": config.get("parameters"), "teststeps_chain_style": [ make_teststep_chain_style(step) for step in teststeps ], @@ -608,22 +619,3 @@ def init_make_parser(subparsers): ) return parser - - -def make_test_start_style(config: Dict) -> Text: - test_start_style = "" - if "parameters" in config.keys(): - 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 d5178356..f7fd4f25 100644 --- a/httprunner/parser.py +++ b/httprunner/parser.py @@ -1,7 +1,7 @@ import ast import builtins import re -from typing import Any, Set, Text, Callable, List, Dict +from typing import Any, Set, Text, Callable, List, Dict, Union from loguru import logger from sentry_sdk import capture_exception @@ -465,51 +465,44 @@ def parse_variables_mapping( return parsed_variables -def parse_parameters(parameters, variables_mapping=None, functions_mapping=None): +def parse_parameters(parameters: Dict,) -> List[Dict]: """ parse parameters and generate cartesian product. Args: - parameters (list) parameters: parameter name and value in list + parameters (Dict) parameters: parameter name and value mapping 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()}"} - ] + >>> 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) """ - variables_mapping = variables_mapping or {} - functions_mapping = functions_mapping or {} - parsed_parameters_list = [] + parsed_parameters_list: List[List[Dict]] = [] # load project_meta functions - from httprunner.loader import load_project_meta - project_meta = load_project_meta("") - functions_mapping.update(project_meta.functions) + project_meta = loader.load_project_meta("") + functions_mapping = project_meta.functions - 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): + 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 = [] + parameter_content_list: List[Dict] = [] for parameter_item in parameter_content: if not isinstance(parameter_item, (list, tuple)): # "2.8.5" => ["2.8.5"] @@ -518,25 +511,21 @@ def parse_parameters(parameters, variables_mapping=None, functions_mapping=None) # ["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: - # (2) & (3) - parsed_variables_mapping = parse_variables_mapping( - variables_mapping, - functions_mapping - ) - parsed_parameter_content = parse_data( - 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 = [] + elif isinstance(parameter_content, Text): + # (2) & (3) + parsed_parameter_content: List = parse_data( + parameter_content, {}, functions_mapping + ) + if not isinstance(parsed_parameter_content, List): + raise exceptions.ParamsError( + f"parameters content should be in List type, got {parsed_parameter_content} for {parameter_content}" + ) + + parameter_content_list: List[Dict] = [] for parameter_item in parsed_parameter_content: - if isinstance(parameter_item, dict): + 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'}] @@ -545,20 +534,39 @@ def parse_parameters(parameters, variables_mapping=None, functions_mapping=None) # {"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)) + parameter_dict: Dict = { + key: parameter_item[key] for key in parameter_name_list + } + elif isinstance(parameter_item, (List, tuple)): + if len(parameter_name_list) == len(parameter_item): + # {"username-password": "${get_account()}"} + # get_account() => [("user1", "111111"), ("user2", "222222")] + parameter_dict = dict(zip(parameter_name_list, parameter_item)) + else: + raise exceptions.ParamsError( + f"parameter names length are not equal to value length.\n" + f"parameter names: {parameter_name_list}\n" + f"parameter values: {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_dict will get: {"user_agent": "iOS/10.1", "user_agent": "iOS/10.2"} + parameter_dict = {parameter_name_list[0]: parameter_item} + else: + raise exceptions.ParamsError( + f"Invalid parameter names and values:\n" + f"parameter names: {parameter_name_list}\n" + f"parameter values: {parameter_item}" + ) parameter_content_list.append(parameter_dict) + else: + raise exceptions.ParamsError( + f"parameter content should be List or Text(variables or functions call), got {parameter_content}" + ) + parsed_parameters_list.append(parameter_content_list) return utils.gen_cartesian_product(*parsed_parameters_list) diff --git a/httprunner/response.py b/httprunner/response.py index 6b129a97..7de9bd03 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -2,12 +2,13 @@ from typing import Dict, Text, Any, NoReturn import jmespath import requests +from jmespath.exceptions import JMESPathError from loguru import logger from httprunner import exceptions from httprunner.exceptions import ValidationFailure, ParamsError -from httprunner.parser import parse_data, parse_string_value, get_mapping_function from httprunner.models import VariablesMapping, Validators, FunctionsMapping +from httprunner.parser import parse_data, parse_string_value, get_mapping_function def get_uniform_comparator(comparator: Text): @@ -143,14 +144,25 @@ class ResponseObject(object): self.__dict__[key] = value return value - @property - def resp_obj_meta(self): - return { + def _search_jmespath(self, expr: Text) -> Any: + resp_obj_meta = { "status_code": self.status_code, "headers": self.headers, "cookies": self.cookies, "body": self.body, } + try: + check_value = jmespath.search(expr, resp_obj_meta) + except JMESPathError as ex: + logger.error( + f"failed to search with jmespath\n" + f"expression: {expr}\n" + f"data: {resp_obj_meta}\n" + f"exception: {ex}" + ) + raise + + return check_value def extract(self, extractors: Dict[Text, Text]) -> Dict[Text, Any]: if not extractors: @@ -158,7 +170,7 @@ class ResponseObject(object): extract_mapping = {} for key, field in extractors.items(): - field_value = jmespath.search(field, self.resp_obj_meta) + field_value = self._search_jmespath(field) extract_mapping[key] = field_value logger.info(f"extract mapping: {extract_mapping}") @@ -197,7 +209,11 @@ class ResponseObject(object): ) check_item = parse_string_value(check_item) - check_value = jmespath.search(check_item, self.resp_obj_meta) + if check_item and isinstance(check_item, Text): + check_value = self._search_jmespath(check_item) + else: + # variable or function evaluation result is "" or not text + check_value = check_item # comparator assert_method = u_validator["assert"] diff --git a/httprunner/runner.py b/httprunner/runner.py index d2129f03..64abf690 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, parametrize=None) -> "HttpRunner": + def test_start(self, param: Dict = None) -> "HttpRunner": """main entrance, discovered by pytest""" self.__init_tests__() self.__project_meta = self.__project_meta or load_project_meta( @@ -435,8 +435,8 @@ class HttpRunner(object): # parse config name config_variables = self.__config.variables - if parametrize: - config_variables.update(parametrize) + if param: + config_variables.update(param) config_variables.update(self.__session_variables) self.__config.name = parse_data( self.__config.name, config_variables, self.__project_meta.functions diff --git a/httprunner/testcase.py b/httprunner/testcase.py index f8044d95..86d1554f 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -159,7 +159,7 @@ class StepRequestValidation(object): return self def assert_string_equals( - self, jmes_path: Text, expected_value: int, message: Text = "" + self, jmes_path: Text, expected_value: Any, message: Text = "" ) -> "StepRequestValidation": self.__step_context.validators.append( {"string_equals": [jmes_path, expected_value, message]} diff --git a/httprunner/utils.py b/httprunner/utils.py index 597c728c..eeb7e0a0 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -6,7 +6,7 @@ import platform import uuid from multiprocessing import Queue import itertools -from typing import Dict, List, Any +from typing import Dict, List, Any, Union, Text import sentry_sdk from loguru import logger @@ -223,42 +223,7 @@ def is_support_multiprocessing() -> bool: 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): +def gen_cartesian_product(*args: List[Dict]) -> List[Dict]: """ generate cartesian product for lists Args: diff --git a/pyproject.toml b/pyproject.toml index e300377b..e207aa77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "3.1.2" +version = "3.1.3" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" diff --git a/tests/make_test.py b/tests/make_test.py index e8e8fb15..56199b8f 100644 --- a/tests/make_test.py +++ b/tests/make_test.py @@ -1,6 +1,7 @@ import os import unittest +from httprunner import loader from httprunner.make import ( main_make, convert_testcase_path, @@ -9,9 +10,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 class TestMake(unittest.TestCase): @@ -215,26 +214,3 @@ 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 96d3cd20..e1eef7fa 100644 --- a/tests/parser_test.py +++ b/tests/parser_test.py @@ -1,9 +1,10 @@ +import os import time import unittest from httprunner import parser from httprunner.exceptions import VariableNotFound, FunctionNotFound -from httprunner.parser import regex_findall_variables +from httprunner.loader import load_project_meta class TestParserBasic(unittest.TestCase): @@ -27,17 +28,19 @@ class TestParserBasic(unittest.TestCase): self.assertEqual(parser.parse_string_value("${func}"), "${func}") def test_regex_findall_variables(self): - self.assertEqual(regex_findall_variables("$variable"), ["variable"]) - self.assertEqual(regex_findall_variables("${variable}123"), ["variable"]) - self.assertEqual(regex_findall_variables("/blog/$postid"), ["postid"]) - self.assertEqual(regex_findall_variables("/$var1/$var2"), ["var1", "var2"]) - self.assertEqual(regex_findall_variables("abc"), []) - self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$a"), ["a"]) - self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$a"), []) - self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$$a"), ["a"]) - self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$$$a"), []) - self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$a$b"), ["b"]) - self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$a$$b"), []) + self.assertEqual(parser.regex_findall_variables("$variable"), ["variable"]) + self.assertEqual(parser.regex_findall_variables("${variable}123"), ["variable"]) + self.assertEqual(parser.regex_findall_variables("/blog/$postid"), ["postid"]) + self.assertEqual( + parser.regex_findall_variables("/$var1/$var2"), ["var1", "var2"] + ) + self.assertEqual(parser.regex_findall_variables("abc"), []) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$a"), ["a"]) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$a"), []) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$$a"), ["a"]) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$$$a"), []) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$a$b"), ["b"]) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$a$$b"), []) def test_extract_variables(self): self.assertEqual(parser.extract_variables("$var"), {"var"}) @@ -454,28 +457,91 @@ class TestParserBasic(unittest.TestCase): self.assertEqual(parsed_testcase["headers"]["sum"], 3) def test_parse_parameters_testcase(self): - variables = { - "user_agent": "chrome", - "sum": 5, + parameters = { + "user_agent": ["iOS/10.1", "iOS/10.2"], + "username-password": "${parameterize(request_methods/account.csv)}", + "sum": "${calculate_two_nums(1, 2)}", } - param = [ + load_project_meta( + os.path.join( + os.path.dirname(os.path.dirname(__file__)), + "examples", + "postman_echo", + "request_methods", + ), + ) + parsed_params = parser.parse_parameters(parameters) + self.assertEqual(len(parsed_params), 2 * 3 * 2) + + self.assertIn( { - "user_agent": ["iOS/10.1", "iOS/10.2"], - "username-password": "${parameterize(request_methods/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) + "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/response_test.py b/tests/response_test.py index f6f8e936..7f98cfa3 100644 --- a/tests/response_test.py +++ b/tests/response_test.py @@ -28,12 +28,28 @@ class TestResponse(unittest.TestCase): self.assertEqual(extract_mapping["var_2"], "Olympia") def test_validate(self): - variables_mapping = {"index": 1} self.resp_obj.validate( [ {"eq": ["body.json.locations[0].name", "Seattle"]}, {"eq": ["body.json.locations[0]", {"name": "Seattle", "state": "WA"}]}, + ], + ) + + def test_validate_variables(self): + variables_mapping = {"index": 1, "var_empty": ""} + self.resp_obj.validate( + [ {"eq": ["body.json.locations[$index].name", "New York"]}, + {"eq": ["$var_empty", ""]}, ], variables_mapping=variables_mapping, ) + + def test_validate_functions(self): + variables_mapping = {"index": 1} + functions_mapping = {"get_num": lambda x: x} + self.resp_obj.validate( + [{"eq": ["${get_num(0)}", 0]}, {"eq": ["${get_num($index)}", 1]},], + variables_mapping=variables_mapping, + functions_mapping=functions_mapping, + ) diff --git a/tests/utils_test.py b/tests/utils_test.py index 2d767e05..444c4f3b 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -126,54 +126,28 @@ class TestUtils(unittest.TestCase): {"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} - ] - ] + parameters_content_list = [[{"a": 1}, {"a": 2}]] product_list = utils.gen_cartesian_product(*parameters_content_list) - self.assertEqual( - product_list, - [ - {"a": 1}, - {"a": 2} - ] - ) + 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} - ] + [{"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} - ] + {"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 + self.assertEqual(product_list, [])