From 092dc299c667c6c13bfb82fd9ad65594262c9db1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 17 May 2020 17:10:44 +0800 Subject: [PATCH 01/20] fix: extract response cookies --- httprunner/response.py | 1 + 1 file changed, 1 insertion(+) diff --git a/httprunner/response.py b/httprunner/response.py index b268d4b5..b739da95 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -124,6 +124,7 @@ class ResponseObject(object): self.resp_obj_meta = { "status_code": resp_obj.status_code, "headers": resp_obj.headers, + "cookies": dict(resp_obj.cookies), "body": body, } self.validation_results: Dict = {} From 1413f5a8a173843ef9eb273010bfef52604b3a63 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 18 May 2020 10:44:00 +0800 Subject: [PATCH 02/20] fix: udpate examples --- docs/CHANGELOG.md | 6 ++++++ examples/httpbin/basic.yml | 6 +++--- examples/httpbin/basic_test.py | 15 ++++++++++++--- examples/httpbin/hooks.yml | 3 ++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d07d72e0..0a5d43ff 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 3.0.4 (2020-05-18) + +**Fixed** + +- fix: extract response cookies + ## 3.0.3 (2020-05-17) **Fixed** diff --git a/examples/httpbin/basic.yml b/examples/httpbin/basic.yml index d27d8604..f0e36fd6 100644 --- a/examples/httpbin/basic.yml +++ b/examples/httpbin/basic.yml @@ -19,7 +19,7 @@ teststeps: method: GET validate: - eq: ["status_code", 200] -# - startswith: [body.user-agent, "python-requests"] + - startswith: [body."user-agent", "python-requests"] - name: get without params @@ -58,7 +58,7 @@ teststeps: method: GET validate: - eq: ["status_code", 200] - # - eq: [cookies.name, "value"] + - eq: [body.cookies.name, "value"] - name: extract cookie @@ -67,7 +67,7 @@ teststeps: method: GET validate: - eq: ["status_code", 200] - # - eq: [cookies.name, "value"] + - eq: [body.cookies.name, "value"] - name: post data diff --git a/examples/httpbin/basic_test.py b/examples/httpbin/basic_test.py index 79634dbb..4d92b82a 100644 --- a/examples/httpbin/basic_test.py +++ b/examples/httpbin/basic_test.py @@ -27,7 +27,10 @@ class TestCaseBasic(HttpRunner): **{ "name": "user-agent", "request": {"url": "/user-agent", "method": "GET"}, - "validate": [{"eq": ["status_code", 200]}], + "validate": [ + {"eq": ["status_code", 200]}, + {"startswith": ['body."user-agent"', "python-requests"]}, + ], } ), TStep( @@ -61,14 +64,20 @@ class TestCaseBasic(HttpRunner): **{ "name": "set cookie", "request": {"url": "/cookies/set?name=value", "method": "GET"}, - "validate": [{"eq": ["status_code", 200]}], + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.cookies.name", "value"]}, + ], } ), TStep( **{ "name": "extract cookie", "request": {"url": "/cookies", "method": "GET"}, - "validate": [{"eq": ["status_code", 200]}], + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.cookies.name", "value"]}, + ], } ), TStep( diff --git a/examples/httpbin/hooks.yml b/examples/httpbin/hooks.yml index 765f5daf..770d9926 100644 --- a/examples/httpbin/hooks.yml +++ b/examples/httpbin/hooks.yml @@ -32,5 +32,6 @@ teststeps: - ${alter_response($response)} validate: - eq: ["status_code", 200] -# - eq: ["headers.content-type", "html/text"] + # TODO: implement hooks +# - eq: [body.headers."Content-Type", "html/text"] - eq: [body.headers.Host, "httpbin.org"] From 1359839c8b14dc07d744765e509b1a16a06275c7 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 18 May 2020 10:57:45 +0800 Subject: [PATCH 03/20] change: load yaml/json error message --- httprunner/loader.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/httprunner/loader.py b/httprunner/loader.py index 96de4c8f..34b4356c 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -34,7 +34,8 @@ def _load_yaml_file(yaml_file: Text) -> Dict: try: yaml_content = yaml.load(stream) except yaml.YAMLError as ex: - logger.error(str(ex)) + err_msg = f"YAMLError:\nfile: {yaml_file}\nerror: {ex}" + logger.error(err_msg) raise exceptions.FileFormatError return yaml_content @@ -46,8 +47,8 @@ def _load_json_file(json_file: Text) -> Dict: with io.open(json_file, encoding="utf-8") as data_file: try: json_content = json.load(data_file) - except json.JSONDecodeError: - err_msg = f"JSONDecodeError: JSON file format error: {json_file}" + except json.JSONDecodeError as ex: + err_msg = f"JSONDecodeError:\nfile: {json_file}\nerror: {ex}" logger.error(err_msg) raise exceptions.FileFormatError(err_msg) From 718fea62e0610b86282f49ef5d1a46fa8203b675 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 18 May 2020 11:00:51 +0800 Subject: [PATCH 04/20] change: validate testcase error message --- httprunner/loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httprunner/loader.py b/httprunner/loader.py index 34b4356c..9cb18d9b 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -75,8 +75,8 @@ def load_testcase_file(testcase_file: Text) -> Tuple[Dict, TestCase]: # validate with pydantic TestCase model testcase_obj = TestCase.parse_obj(testcase_content) except ValidationError as ex: - err_msg = f"Invalid testcase format: {testcase_file}" - logger.error(f"{err_msg}\n{ex}") + err_msg = f"TestCase ValidationError:\nfile: {testcase_file}\nerror: {ex}" + logger.error(err_msg) raise exceptions.TestCaseFormatError(err_msg) testcase_content["config"]["path"] = testcase_file From a17ff355a299eaa7fb21d2a233c57272becbcfa6 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 18 May 2020 11:25:49 +0800 Subject: [PATCH 05/20] fix: handle errors when no valid testcases generated --- docs/CHANGELOG.md | 1 + httprunner/cli.py | 6 +++++- httprunner/ext/make/__init__.py | 8 ++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0a5d43ff..7580ec70 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,7 @@ **Fixed** - fix: extract response cookies +- fix: handle errors when no valid testcases generated ## 3.0.3 (2020-05-17) diff --git a/httprunner/cli.py b/httprunner/cli.py index 34d9d0ff..10bbda97 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -3,6 +3,7 @@ import os import sys import pytest +from loguru import logger from httprunner import __description__, __version__, exceptions from httprunner.ext.har2case import init_har2case_parser, main_har2case @@ -33,7 +34,10 @@ def main_run(extra_args): # has not specified any testcase path raise exceptions.ParamsError("Missed testcase path") - main_make(tests_path_list) + testcase_path_list = main_make(tests_path_list) + if not testcase_path_list: + logger.error("No valid testcases found, exit 1.") + sys.exit(1) if "-s" not in extra_args: extra_args.insert(0, "-s") diff --git a/httprunner/ext/make/__init__.py b/httprunner/ext/make/__init__.py index 7da40807..48c4aeaf 100644 --- a/httprunner/ext/make/__init__.py +++ b/httprunner/ext/make/__init__.py @@ -89,7 +89,7 @@ def format_with_black(tests_path: Text): logger.error(ex) -def make(tests_path: Text) -> List: +def __make(tests_path: Text) -> List: testcases = [] if os.path.isdir(tests_path): files_list = load_folder_files(tests_path) @@ -106,6 +106,10 @@ def make(tests_path: Text) -> List: continue testcase_path_list.append(testcase_path) + if not testcase_path_list: + logger.warning(f"No valid testcase generated on {tests_path}") + return [] + format_with_black(tests_path) return testcase_path_list @@ -113,7 +117,7 @@ def make(tests_path: Text) -> List: def main_make(tests_paths: List[Text]) -> List: testcase_path_list = [] for tests_path in tests_paths: - testcase_path_list.extend(make(tests_path)) + testcase_path_list.extend(__make(tests_path)) return testcase_path_list From c9039535da8fdb876b36169ffba5eaa20ba8b6c6 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 18 May 2020 13:07:57 +0800 Subject: [PATCH 06/20] change: split load_test_file --- httprunner/loader.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/httprunner/loader.py b/httprunner/loader.py index 9cb18d9b..5dbdc61e 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -55,22 +55,29 @@ def _load_json_file(json_file: Text) -> Dict: return json_content -def load_testcase_file(testcase_file: Text) -> Tuple[Dict, TestCase]: - """load testcase file and validate with pydantic model""" - if not os.path.isfile(testcase_file): - raise exceptions.FileNotFound(f"testcase file not exists: {testcase_file}") +def load_test_file(test_file: Text) -> Dict: + """load testcase/testsuite file content""" + if not os.path.isfile(test_file): + raise exceptions.FileNotFound(f"test file not exists: {test_file}") - file_suffix = os.path.splitext(testcase_file)[1].lower() + file_suffix = os.path.splitext(test_file)[1].lower() if file_suffix == ".json": - testcase_content = _load_json_file(testcase_file) + test_file_content = _load_json_file(test_file) elif file_suffix in [".yaml", ".yml"]: - testcase_content = _load_yaml_file(testcase_file) + test_file_content = _load_yaml_file(test_file) else: # '' or other suffix raise exceptions.FileFormatError( - f"testcase file should be YAML/JSON format, invalid testcase file: {testcase_file}" + f"testcase/testsuite file should be YAML/JSON format, invalid format file: {test_file}" ) + return test_file_content + + +def load_testcase_file(testcase_file: Text) -> Tuple[Dict, TestCase]: + """load testcase file and validate with pydantic model""" + testcase_content = load_test_file(testcase_file) + try: # validate with pydantic TestCase model testcase_obj = TestCase.parse_obj(testcase_content) From bd71a238439e1d2beec287bb8a49d3a403f05e96 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 18 May 2020 15:55:38 +0800 Subject: [PATCH 07/20] refactor: make testcase --- httprunner/ext/make/__init__.py | 50 +++++++++++++++++++++++--------- httprunner/ext/make/make_test.py | 34 +++++++++------------- httprunner/loader.py | 21 ++++++++------ httprunner/loader_test.py | 6 +--- httprunner/runner.py | 2 +- 5 files changed, 63 insertions(+), 50 deletions(-) diff --git a/httprunner/ext/make/__init__.py b/httprunner/ext/make/__init__.py index 48c4aeaf..3ca737aa 100644 --- a/httprunner/ext/make/__init__.py +++ b/httprunner/ext/make/__init__.py @@ -1,13 +1,18 @@ import os import subprocess -from typing import Union, Text, List, Tuple +from typing import Union, Text, List, Tuple, Dict import jinja2 from loguru import logger from httprunner import exceptions from httprunner.exceptions import TestCaseFormatError -from httprunner.loader import load_testcase_file, load_folder_files +from httprunner.loader import ( + load_testcase_file, + load_folder_files, + load_test_file, + load_testcase, +) __TMPL__ = """# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # FROM: {{ testcase_path }} @@ -51,12 +56,9 @@ def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]: return testcase_python_path, name_in_title_case -def make_testcase(testcase_path: str) -> Union[str, None]: +def make_testcase(testcase: Dict) -> Union[str, None]: + testcase_path = testcase["config"]["path"] logger.info(f"start to make testcase: {testcase_path}") - try: - testcase, _ = load_testcase_file(testcase_path) - except TestCaseFormatError: - return None template = jinja2.Template(__TMPL__) @@ -90,21 +92,41 @@ def format_with_black(tests_path: Text): def __make(tests_path: Text) -> List: - testcases = [] + test_files = [] if os.path.isdir(tests_path): files_list = load_folder_files(tests_path) - testcases.extend(files_list) + test_files.extend(files_list) elif os.path.isfile(tests_path): - testcases.append(tests_path) + test_files.append(tests_path) else: raise exceptions.TestcaseNotFound(f"Invalid tests path: {tests_path}") testcase_path_list = [] - for testcase_path in testcases: - testcase_path = make_testcase(testcase_path) - if not testcase_path: + for test_file in test_files: + try: + test_content = load_test_file(test_file) + test_content.setdefault("config", {})["path"] = test_file + except (exceptions.FileNotFound, exceptions.FileFormatError) as ex: + logger.warning(ex) continue - testcase_path_list.append(testcase_path) + + if "teststeps" in test_content: + # testcase + try: + # validate testcase format + load_testcase(test_content) + except exceptions.TestCaseFormatError: + continue + + testcase_file = make_testcase(test_content) + testcase_path_list.append(testcase_file) + elif "testcases" in test_content: + # testsuite + pass + else: + raise exceptions.FileFormatError( + f"test file is neither testcase nor testsuite: {test_file}" + ) if not testcase_path_list: logger.warning(f"No valid testcase generated on {tests_path}") diff --git a/httprunner/ext/make/make_test.py b/httprunner/ext/make/make_test.py index 44e309a6..ecc3fcef 100644 --- a/httprunner/ext/make/make_test.py +++ b/httprunner/ext/make/make_test.py @@ -4,10 +4,10 @@ from httprunner.ext.make import make_testcase, main_make, convert_testcase_path class TestLoader(unittest.TestCase): def test_make_testcase(self): - path = "examples/postman_echo/request_methods/request_with_variables.yml" - testcase_python_path = make_testcase(path) + path = ["examples/postman_echo/request_methods/request_with_variables.yml"] + testcase_python_list = main_make(path) self.assertEqual( - testcase_python_path, + testcase_python_list[0], "examples/postman_echo/request_methods/request_with_variables_test.py", ) @@ -21,42 +21,34 @@ class TestLoader(unittest.TestCase): def test_convert_testcase_path(self): self.assertEqual( - convert_testcase_path("mubu.login.yml")[0], - "mubu_login_test.py" + convert_testcase_path("mubu.login.yml")[0], "mubu_login_test.py" ) self.assertEqual( convert_testcase_path("/path/to/mubu.login.yml")[0], - "/path/to/mubu_login_test.py" + "/path/to/mubu_login_test.py", ) self.assertEqual( convert_testcase_path("/path/to 2/mubu.login.yml")[0], - "/path/to 2/mubu_login_test.py" + "/path/to 2/mubu_login_test.py", ) self.assertEqual( - convert_testcase_path("/path/to 2/mubu.login.yml")[1], - "MubuLogin" + convert_testcase_path("/path/to 2/mubu.login.yml")[1], "MubuLogin" ) self.assertEqual( - convert_testcase_path("mubu login.yml")[0], - "mubu_login_test.py" + convert_testcase_path("mubu login.yml")[0], "mubu_login_test.py" ) self.assertEqual( - convert_testcase_path("/path/to 2/mubu login.yml")[1], - "MubuLogin" + convert_testcase_path("/path/to 2/mubu login.yml")[1], "MubuLogin" ) self.assertEqual( convert_testcase_path("/path/to 2/mubu-login.yml")[0], - "/path/to 2/mubu_login_test.py" + "/path/to 2/mubu_login_test.py", ) self.assertEqual( - convert_testcase_path("/path/to 2/mubu-login.yml")[1], - "MubuLogin" + convert_testcase_path("/path/to 2/mubu-login.yml")[1], "MubuLogin" ) self.assertEqual( convert_testcase_path("/path/to 2/幕布login.yml")[0], - "/path/to 2/幕布login_test.py" - ) - self.assertEqual( - convert_testcase_path("/path/to/幕布login.yml")[1], - "幕布Login" + "/path/to 2/幕布login_test.py", ) + self.assertEqual(convert_testcase_path("/path/to/幕布login.yml")[1], "幕布Login") diff --git a/httprunner/loader.py b/httprunner/loader.py index 5dbdc61e..e2b1d91e 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -74,22 +74,25 @@ def load_test_file(test_file: Text) -> Dict: return test_file_content -def load_testcase_file(testcase_file: Text) -> Tuple[Dict, TestCase]: - """load testcase file and validate with pydantic model""" - testcase_content = load_test_file(testcase_file) - +def load_testcase(testcase: Dict) -> TestCase: + path = testcase["config"]["path"] try: # validate with pydantic TestCase model - testcase_obj = TestCase.parse_obj(testcase_content) + testcase_obj = TestCase.parse_obj(testcase) except ValidationError as ex: - err_msg = f"TestCase ValidationError:\nfile: {testcase_file}\nerror: {ex}" + err_msg = f"TestCase ValidationError:\nfile: {path}\nerror: {ex}" logger.error(err_msg) raise exceptions.TestCaseFormatError(err_msg) - testcase_content["config"]["path"] = testcase_file - testcase_obj.config.path = testcase_file + return testcase_obj - return testcase_content, testcase_obj + +def load_testcase_file(testcase_file: Text) -> TestCase: + """load testcase file and validate with pydantic model""" + testcase_content = load_test_file(testcase_file) + testcase_content.setdefault("config", {})["path"] = testcase_file + testcase_obj = load_testcase(testcase_content) + return testcase_obj def load_dot_env_file(dot_env_path: Text) -> Dict: diff --git a/httprunner/loader_test.py b/httprunner/loader_test.py index dac77804..c5e2ab36 100644 --- a/httprunner/loader_test.py +++ b/httprunner/loader_test.py @@ -7,14 +7,10 @@ from httprunner import exceptions, loader class TestLoader(unittest.TestCase): def test_load_testcase_file(self): path = "examples/postman_echo/request_methods/request_with_variables.yml" - testcase_json, testcase_obj = loader.load_testcase_file(path) - self.assertEqual( - testcase_json["config"]["name"], "request methods testcase with variables" - ) + testcase_obj = loader.load_testcase_file(path) self.assertEqual( testcase_obj.config.name, "request methods testcase with variables" ) - self.assertEqual(len(testcase_json["teststeps"]), 3) self.assertEqual(len(testcase_obj.teststeps), 3) def test_load_json_file_file_format_error(self): diff --git a/httprunner/runner.py b/httprunner/runner.py index ab833ead..7095c145 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -209,7 +209,7 @@ class HttpRunner(object): if not os.path.isfile(path): raise exceptions.ParamsError(f"Invalid testcase path: {path}") - _, testcase_obj = load_testcase_file(path) + testcase_obj = load_testcase_file(path) return self.run(testcase_obj) def get_step_datas(self) -> List[StepData]: From 5b7bcea3d08ffd21c65e6fbc9f396fa7626fdd09 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 18 May 2020 19:38:39 +0800 Subject: [PATCH 08/20] feat: make testsuite and run testsuite --- docs/CHANGELOG.md | 4 + .../request_methods/demo_testsuite.yml | 15 +++ .../request_with_functions_test.py | 89 +++++++++++++++++ .../request_with_testcase_reference_test.py | 29 ++++++ httprunner/cli.py | 21 ++-- httprunner/exceptions.py | 4 + httprunner/ext/make/__init__.py | 99 +++++++++++++++---- httprunner/ext/make/make_test.py | 16 ++- httprunner/loader.py | 23 ++++- httprunner/schema.py | 12 +++ 10 files changed, 283 insertions(+), 29 deletions(-) create mode 100644 examples/postman_echo/request_methods/demo_testsuite.yml create mode 100644 examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py create mode 100644 examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7580ec70..39e42075 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,10 @@ ## 3.0.4 (2020-05-18) +**Added** + +- feat: make testsuite and run testsuite + **Fixed** - fix: extract response cookies diff --git a/examples/postman_echo/request_methods/demo_testsuite.yml b/examples/postman_echo/request_methods/demo_testsuite.yml new file mode 100644 index 00000000..b96d04ca --- /dev/null +++ b/examples/postman_echo/request_methods/demo_testsuite.yml @@ -0,0 +1,15 @@ +config: + name: "demo testsuite" +# variables: ${get_variable()} + +testcases: +- + name: request with functions + testcase: request_methods/request_with_functions.yml + variables: + var1: testsuite_val1 +- + name: request with referenced testcase + testcase: request_methods/request_with_testcase_reference.yml + variables: + var2: testsuite_val2 diff --git a/examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py b/examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py new file mode 100644 index 00000000..aaec48f9 --- /dev/null +++ b/examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py @@ -0,0 +1,89 @@ +# NOTICE: Generated By HttpRunner. DO'NOT EDIT! +# FROM: examples/postman_echo/request_methods/demo_testsuite/request_with_functions.yml +from httprunner import HttpRunner, TConfig, TStep + + +class TestCaseRequestWithFunctions(HttpRunner): + config = TConfig( + **{ + "name": "request with functions", + "variables": {"foo1": "session_bar1", "var1": "testsuite_val1"}, + "base_url": "https://postman-echo.com", + "verify": False, + "path": "examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py", + } + ) + + teststeps = [ + TStep( + **{ + "name": "get with params", + "variables": { + "foo1": "bar1", + "foo2": "session_bar2", + "sum_v": "${sum_two(1, 2)}", + }, + "request": { + "method": "GET", + "url": "/get", + "params": {"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}, + "headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"}, + }, + "extract": {"session_foo2": "body.args.foo2"}, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.args.foo1", "session_bar1"]}, + {"eq": ["body.args.sum_v", 3]}, + {"eq": ["body.args.foo2", "session_bar2"]}, + ], + } + ), + TStep( + **{ + "name": "post raw text", + "variables": {"foo1": "hello world", "foo3": "$session_foo2"}, + "request": { + "method": "POST", + "url": "/post", + "headers": { + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "text/plain", + }, + "data": "This is expected to be sent back as part of response body: $foo1-$foo3.", + }, + "validate": [ + {"eq": ["status_code", 200]}, + { + "eq": [ + "body.data", + "This is expected to be sent back as part of response body: session_bar1-session_bar2.", + ] + }, + ], + } + ), + TStep( + **{ + "name": "post form data", + "variables": {"foo1": "bar1", "foo2": "bar2"}, + "request": { + "method": "POST", + "url": "/post", + "headers": { + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + }, + "data": "foo1=$foo1&foo2=$foo2", + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.form.foo1", "session_bar1"]}, + {"eq": ["body.form.foo2", "bar2"]}, + ], + } + ), + ] + + +if __name__ == "__main__": + TestCaseRequestWithFunctions().test_start() diff --git a/examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py new file mode 100644 index 00000000..bb53226e --- /dev/null +++ b/examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py @@ -0,0 +1,29 @@ +# NOTICE: Generated By HttpRunner. DO'NOT EDIT! +# FROM: examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference.yml +from httprunner import HttpRunner, TConfig, TStep + + +class TestCaseRequestWithTestcaseReference(HttpRunner): + config = TConfig( + **{ + "name": "request with referenced testcase", + "variables": {"foo1": "session_bar1", "var2": "testsuite_val2"}, + "base_url": "https://postman-echo.com", + "verify": False, + "path": "examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py", + } + ) + + teststeps = [ + TStep( + **{ + "name": "request with variables", + "variables": {"foo1": "override_bar1"}, + "testcase": "request_methods/request_with_variables.yml", + } + ), + ] + + +if __name__ == "__main__": + TestCaseRequestWithTestcaseReference().test_start() diff --git a/httprunner/cli.py b/httprunner/cli.py index 10bbda97..68f67b21 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -20,15 +20,14 @@ def init_parser_run(subparsers): def main_run(extra_args): tests_path_list = [] - for index, item in enumerate(extra_args): + extra_args_new = [] + for item in extra_args: if not os.path.exists(item): # item is not file/folder path - continue - elif os.path.isfile(item): - # replace YAML/JSON file path with generated python file - extra_args[index], _ = convert_testcase_path(item) - - tests_path_list.append(item) + extra_args_new.append(item) + else: + # item is file/folder path + tests_path_list.append(item) if len(tests_path_list) == 0: # has not specified any testcase path @@ -39,9 +38,11 @@ def main_run(extra_args): logger.error("No valid testcases found, exit 1.") sys.exit(1) - if "-s" not in extra_args: - extra_args.insert(0, "-s") - pytest.main(extra_args) + extra_args_new.extend(testcase_path_list) + if "-s" not in extra_args_new: + extra_args_new.insert(0, "-s") + + pytest.main(extra_args_new) def main(): diff --git a/httprunner/exceptions.py b/httprunner/exceptions.py index 66e8f7a5..39e45f7e 100644 --- a/httprunner/exceptions.py +++ b/httprunner/exceptions.py @@ -44,6 +44,10 @@ class TestCaseFormatError(MyBaseError): pass +class TestSuiteFormatError(MyBaseError): + pass + + class ParamsError(MyBaseError): pass diff --git a/httprunner/ext/make/__init__.py b/httprunner/ext/make/__init__.py index 3ca737aa..4ddfd977 100644 --- a/httprunner/ext/make/__init__.py +++ b/httprunner/ext/make/__init__.py @@ -6,12 +6,12 @@ import jinja2 from loguru import logger from httprunner import exceptions -from httprunner.exceptions import TestCaseFormatError from httprunner.loader import ( - load_testcase_file, load_folder_files, load_test_file, load_testcase, + load_testsuite, + get_project_working_directory, ) __TMPL__ = """# NOTICE: Generated By HttpRunner. DO'NOT EDIT! @@ -44,7 +44,9 @@ def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]: file_suffix = file_suffix.lower() if file_suffix not in [".json", ".yml", ".yaml"]: - raise exceptions.ParamsError("") + raise exceptions.ParamsError( + "testcase file should have .yaml/.yml/.json suffix" + ) file_name = raw_file_name.replace(" ", "_").replace(".", "_").replace("-", "_") testcase_dir = os.path.dirname(testcase_path) @@ -56,7 +58,23 @@ def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]: return testcase_python_path, name_in_title_case +def format_pytest_with_black(python_path: Text): + logger.info(f"format pytest case with black: {python_path}") + try: + subprocess.run(["black", python_path]) + except subprocess.CalledProcessError as ex: + logger.error(ex) + + def make_testcase(testcase: Dict) -> Union[str, None]: + """convert valid testcase dict to pytest file path""" + try: + # validate testcase format + load_testcase(testcase) + except exceptions.TestCaseFormatError as ex: + logger.error(f"TestCaseFormatError: {ex}") + raise + testcase_path = testcase["config"]["path"] logger.info(f"start to make testcase: {testcase_path}") @@ -74,21 +92,63 @@ def make_testcase(testcase: Dict) -> Union[str, None]: } content = template.render(data) + os.makedirs(os.path.dirname(testcase_python_path), exist_ok=True) with open(testcase_python_path, "w") as f: f.write(content) logger.info(f"generated testcase: {testcase_python_path}") + format_pytest_with_black(testcase_python_path) return testcase_python_path -def format_with_black(tests_path: Text): - logger.info("format testcases with black ...") - tests_path, _ = convert_testcase_path(tests_path) - +def make_testsuite(testsuite: Dict) -> List[Text]: + """convert valid testsuite dict to pytest folder with testcases""" try: - subprocess.run(["black", tests_path]) - except subprocess.CalledProcessError as ex: - logger.error(ex) + # validate testcase format + load_testsuite(testsuite) + except exceptions.TestSuiteFormatError as ex: + logger.error(f"TestSuiteFormatError: {ex}") + raise + + config = testsuite["config"] + testsuite_path = config["path"] + logger.info(f"start to make testsuite: {testsuite_path}") + + testcase_files = [] + project_working_directory = get_project_working_directory(testsuite_path) + + for testcase in testsuite["testcases"]: + # get referenced testcase content + testcase_file = testcase["testcase"] + testcase_path = os.path.join(project_working_directory, testcase_file) + testcase_dict = load_test_file(testcase_path) + testcase_dict.setdefault("config", {}) + + # override testcase name + testcase_dict["config"]["name"] = testcase["name"] + # override base_url + base_url = testsuite["config"].get("base_url") or testcase.get("base_url") + if base_url: + testcase_dict["config"]["base_url"] = base_url + # override variables + testcase_dict["config"].setdefault("variables", {}) + testcase_dict["config"]["variables"].update( + testcase.get("variables", {}) + ) + testcase_dict["config"]["variables"].update( + testsuite["config"].get("variables", {}) + ) + + # create directory with testsuite file name, put its testcases under this directory + testcase_dict["config"]["path"] = os.path.join( + os.path.splitext(testsuite_path)[0], os.path.basename(testcase_path) + ) + + # make testcase + testcase_path = make_testcase(testcase_dict) + testcase_files.append(testcase_path) + + return testcase_files def __make(tests_path: Text) -> List: @@ -110,19 +170,25 @@ def __make(tests_path: Text) -> List: logger.warning(ex) continue + # testcase if "teststeps" in test_content: - # testcase try: - # validate testcase format - load_testcase(test_content) + testcase_file = make_testcase(test_content) except exceptions.TestCaseFormatError: continue - testcase_file = make_testcase(test_content) testcase_path_list.append(testcase_file) + + # testsuite elif "testcases" in test_content: - # testsuite - pass + try: + testcase_files = make_testsuite(test_content) + except exceptions.TestSuiteFormatError: + continue + + testcase_path_list.extend(testcase_files) + + # invalid format else: raise exceptions.FileFormatError( f"test file is neither testcase nor testsuite: {test_file}" @@ -132,7 +198,6 @@ def __make(tests_path: Text) -> List: logger.warning(f"No valid testcase generated on {tests_path}") return [] - format_with_black(tests_path) return testcase_path_list diff --git a/httprunner/ext/make/make_test.py b/httprunner/ext/make/make_test.py index ecc3fcef..7de86252 100644 --- a/httprunner/ext/make/make_test.py +++ b/httprunner/ext/make/make_test.py @@ -1,5 +1,6 @@ import unittest -from httprunner.ext.make import make_testcase, main_make, convert_testcase_path + +from httprunner.ext.make import main_make, convert_testcase_path class TestLoader(unittest.TestCase): @@ -52,3 +53,16 @@ class TestLoader(unittest.TestCase): "/path/to 2/幕布login_test.py", ) self.assertEqual(convert_testcase_path("/path/to/幕布login.yml")[1], "幕布Login") + + def test_make_testsuite(self): + path = ["examples/postman_echo/request_methods/demo_testsuite.yml"] + testcase_python_list = main_make(path) + self.assertEqual(len(testcase_python_list), 2) + self.assertIn( + "examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py", + testcase_python_list, + ) + self.assertIn( + "examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py", + testcase_python_list, + ) diff --git a/httprunner/loader.py b/httprunner/loader.py index e2b1d91e..937fabb0 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -13,7 +13,7 @@ from pydantic import ValidationError from httprunner import builtin, utils from httprunner import exceptions -from httprunner.schema import TestCase, ProjectMeta +from httprunner.schema import TestCase, ProjectMeta, TestSuite try: # PyYAML version >= 5.1 @@ -95,6 +95,19 @@ def load_testcase_file(testcase_file: Text) -> TestCase: return testcase_obj +def load_testsuite(testsuite: Dict) -> TestSuite: + path = testsuite["config"]["path"] + try: + # validate with pydantic TestCase model + testsuite_obj = TestSuite.parse_obj(testsuite) + except ValidationError as ex: + err_msg = f"TestSuite ValidationError:\nfile: {path}\nerror: {ex}" + logger.error(err_msg) + raise exceptions.TestSuiteFormatError(err_msg) + + return testsuite_obj + + def load_dot_env_file(dot_env_path: Text) -> Dict: """ load .env file. @@ -360,6 +373,14 @@ def init_project_working_directory(test_path: Text) -> Tuple[Text, Text]: return debugtalk_path, project_working_directory +def get_project_working_directory(test_path: Text) -> Text: + global project_working_directory + if not project_working_directory: + init_project_working_directory(test_path) + + return project_working_directory + + def load_debugtalk_functions() -> Dict[Text, Callable]: """ load project debugtalk.py module functions debugtalk.py should be located in project working directory. diff --git a/httprunner/schema.py b/httprunner/schema.py index 4ea1e797..b56bedfa 100644 --- a/httprunner/schema.py +++ b/httprunner/schema.py @@ -160,6 +160,18 @@ class PlatformInfo(BaseModel): platform: Text +class TestCaseRef(BaseModel): + name: Text + base_url: Text = "" + testcase: Text + variables: VariablesMapping = {} + + +class TestSuite(BaseModel): + config: TConfig + testcases: List[TestCaseRef] + + class Stat(BaseModel): total: int = 0 success: int = 0 From 7ebb1696c72366ea41fb7395dc60c029362c5206 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 18 May 2020 22:37:12 +0800 Subject: [PATCH 09/20] feat: testsuite config get variables by call function --- examples/postman_echo/debugtalk.py | 6 +++++ .../request_methods/demo_testsuite.yml | 2 +- httprunner/ext/make/__init__.py | 22 ++++++++++++------- httprunner/schema.py | 3 ++- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/examples/postman_echo/debugtalk.py b/examples/postman_echo/debugtalk.py index 849bd537..d0502409 100644 --- a/examples/postman_echo/debugtalk.py +++ b/examples/postman_echo/debugtalk.py @@ -7,3 +7,9 @@ def get_httprunner_version(): def sum_two(m, n): return m + n + + +def get_variables(): + return { + "foo1": "session_bar1" + } diff --git a/examples/postman_echo/request_methods/demo_testsuite.yml b/examples/postman_echo/request_methods/demo_testsuite.yml index b96d04ca..946d37d7 100644 --- a/examples/postman_echo/request_methods/demo_testsuite.yml +++ b/examples/postman_echo/request_methods/demo_testsuite.yml @@ -1,6 +1,6 @@ config: name: "demo testsuite" -# variables: ${get_variable()} + variables: ${get_variables()} testcases: - diff --git a/httprunner/ext/make/__init__.py b/httprunner/ext/make/__init__.py index 4ddfd977..b9fac1de 100644 --- a/httprunner/ext/make/__init__.py +++ b/httprunner/ext/make/__init__.py @@ -11,8 +11,9 @@ from httprunner.loader import ( load_test_file, load_testcase, load_testsuite, - get_project_working_directory, + load_project_meta, ) +from httprunner.parser import parse_data __TMPL__ = """# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # FROM: {{ testcase_path }} @@ -112,10 +113,19 @@ def make_testsuite(testsuite: Dict) -> List[Text]: config = testsuite["config"] testsuite_path = config["path"] + project_meta = load_project_meta(testsuite_path) + project_working_directory = project_meta.PWD + + testsuite_variables = config.get("variables", {}) + if isinstance(testsuite_variables, Text): + # get variables by function, e.g. ${get_variables()} + testsuite_variables = parse_data( + testsuite_variables, {}, project_meta.functions + ) + logger.info(f"start to make testsuite: {testsuite_path}") testcase_files = [] - project_working_directory = get_project_working_directory(testsuite_path) for testcase in testsuite["testcases"]: # get referenced testcase content @@ -132,12 +142,8 @@ def make_testsuite(testsuite: Dict) -> List[Text]: testcase_dict["config"]["base_url"] = base_url # override variables testcase_dict["config"].setdefault("variables", {}) - testcase_dict["config"]["variables"].update( - testcase.get("variables", {}) - ) - testcase_dict["config"]["variables"].update( - testsuite["config"].get("variables", {}) - ) + testcase_dict["config"]["variables"].update(testcase.get("variables", {})) + testcase_dict["config"]["variables"].update(testsuite_variables) # create directory with testsuite file name, put its testcases under this directory testcase_dict["config"]["path"] = os.path.join( diff --git a/httprunner/schema.py b/httprunner/schema.py index b56bedfa..feae1fb5 100644 --- a/httprunner/schema.py +++ b/httprunner/schema.py @@ -36,7 +36,8 @@ class TConfig(BaseModel): name: Name verify: Verify = False base_url: BaseUrl = "" - variables: VariablesMapping = {} + # Text: prepare variables in debugtalk.py, ${get_variable()} + variables: Union[VariablesMapping, Text] = {} setup_hooks: Hook = [] teardown_hooks: Hook = [] export: Export = [] From 9dcf54a5a6bd2302b83be90d1f7c9c24a70b70e0 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 18 May 2020 22:49:58 +0800 Subject: [PATCH 10/20] feat: testcase config get variables by call function --- .../request_methods/request_with_variables.yml | 3 +-- httprunner/ext/make/__init__.py | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/postman_echo/request_methods/request_with_variables.yml b/examples/postman_echo/request_methods/request_with_variables.yml index 625e240f..b5d19cc2 100644 --- a/examples/postman_echo/request_methods/request_with_variables.yml +++ b/examples/postman_echo/request_methods/request_with_variables.yml @@ -1,7 +1,6 @@ config: name: "request methods testcase with variables" - variables: - foo1: session_bar1 + variables: ${get_variables()} base_url: "https://postman-echo.com" verify: False diff --git a/httprunner/ext/make/__init__.py b/httprunner/ext/make/__init__.py index b9fac1de..9b5d437b 100644 --- a/httprunner/ext/make/__init__.py +++ b/httprunner/ext/make/__init__.py @@ -84,6 +84,15 @@ def make_testcase(testcase: Dict) -> Union[str, None]: testcase_python_path, name_in_title_case = convert_testcase_path(testcase_path) config = testcase["config"] + + config.setdefault("variables", {}) + if isinstance(config["variables"], Text): + # get variables by function, e.g. ${get_variables()} + project_meta = load_project_meta(testcase_path) + config["variables"] = parse_data( + config["variables"], {}, project_meta.functions + ) + config["path"] = testcase_python_path data = { "testcase_path": testcase_path, From 3c48078c22fe35c04019e42b6453b5d1a8e8ec84 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 18 May 2020 23:18:49 +0800 Subject: [PATCH 11/20] test: update examples --- .../request_with_testcase_reference_test.py | 4 ++-- .../postman_echo/request_methods/hardcode_test.py | 1 + .../request_with_testcase_reference.yml | 4 ++-- .../request_with_testcase_reference_test.py | 4 ++-- httprunner/ext/make/__init__.py | 12 ++++++------ httprunner/runner_test.py | 6 +++--- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py index bb53226e..fae547cd 100644 --- a/examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py @@ -17,9 +17,9 @@ class TestCaseRequestWithTestcaseReference(HttpRunner): teststeps = [ TStep( **{ - "name": "request with variables", + "name": "request with functions", "variables": {"foo1": "override_bar1"}, - "testcase": "request_methods/request_with_variables.yml", + "testcase": "request_methods/request_with_functions.yml", } ), ] diff --git a/examples/postman_echo/request_methods/hardcode_test.py b/examples/postman_echo/request_methods/hardcode_test.py index e60254c4..934c0d66 100644 --- a/examples/postman_echo/request_methods/hardcode_test.py +++ b/examples/postman_echo/request_methods/hardcode_test.py @@ -10,6 +10,7 @@ class TestCaseHardcode(HttpRunner): "base_url": "https://postman-echo.com", "verify": False, "path": "examples/postman_echo/request_methods/hardcode_test.py", + "variables": {}, } ) diff --git a/examples/postman_echo/request_methods/request_with_testcase_reference.yml b/examples/postman_echo/request_methods/request_with_testcase_reference.yml index 7e139535..3b2bfbdc 100644 --- a/examples/postman_echo/request_methods/request_with_testcase_reference.yml +++ b/examples/postman_echo/request_methods/request_with_testcase_reference.yml @@ -7,7 +7,7 @@ config: teststeps: - - name: request with variables + name: request with functions variables: foo1: override_bar1 - testcase: request_methods/request_with_variables.yml + testcase: request_methods/request_with_functions.yml 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 55b3db9c..229c04fc 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 @@ -17,9 +17,9 @@ class TestCaseRequestWithTestcaseReference(HttpRunner): teststeps = [ TStep( **{ - "name": "request with variables", + "name": "request with functions", "variables": {"foo1": "override_bar1"}, - "testcase": "request_methods/request_with_variables.yml", + "testcase": "request_methods/request_with_functions.yml", } ), ] diff --git a/httprunner/ext/make/__init__.py b/httprunner/ext/make/__init__.py index 9b5d437b..a4df826a 100644 --- a/httprunner/ext/make/__init__.py +++ b/httprunner/ext/make/__init__.py @@ -102,7 +102,6 @@ def make_testcase(testcase: Dict) -> Union[str, None]: } content = template.render(data) - os.makedirs(os.path.dirname(testcase_python_path), exist_ok=True) with open(testcase_python_path, "w") as f: f.write(content) @@ -134,6 +133,9 @@ def make_testsuite(testsuite: Dict) -> List[Text]: logger.info(f"start to make testsuite: {testsuite_path}") + # create directory with testsuite file name, put its testcases under this directory + os.makedirs(os.path.dirname(os.path.splitext(testsuite_path)[0]), exist_ok=True) + testcase_files = [] for testcase in testsuite["testcases"]: @@ -142,6 +144,9 @@ def make_testsuite(testsuite: Dict) -> List[Text]: testcase_path = os.path.join(project_working_directory, testcase_file) testcase_dict = load_test_file(testcase_path) testcase_dict.setdefault("config", {}) + testcase_dict["config"]["path"] = os.path.join( + os.path.splitext(testsuite_path)[0], os.path.basename(testcase_path) + ) # override testcase name testcase_dict["config"]["name"] = testcase["name"] @@ -154,11 +159,6 @@ def make_testsuite(testsuite: Dict) -> List[Text]: testcase_dict["config"]["variables"].update(testcase.get("variables", {})) testcase_dict["config"]["variables"].update(testsuite_variables) - # create directory with testsuite file name, put its testcases under this directory - testcase_dict["config"]["path"] = os.path.join( - os.path.splitext(testsuite_path)[0], os.path.basename(testcase_path) - ) - # make testcase testcase_path = make_testcase(testcase_dict) testcase_files.append(testcase_path) diff --git a/httprunner/runner_test.py b/httprunner/runner_test.py index ca84584f..39d9d978 100644 --- a/httprunner/runner_test.py +++ b/httprunner/runner_test.py @@ -9,11 +9,11 @@ class TestHttpRunner(unittest.TestCase): def test_run_testcase_by_path_request_only(self): self.runner.run_path( - "examples/postman_echo/request_methods/request_with_variables.yml" + "examples/postman_echo/request_methods/request_with_functions.yml" ) result = self.runner.get_summary() self.assertTrue(result.success) - self.assertEqual(result.name, "request methods testcase with variables") + self.assertEqual(result.name, "request methods testcase with functions") self.assertEqual(result.step_datas[0].name, "get with params") self.assertEqual(len(result.step_datas), 3) @@ -24,5 +24,5 @@ class TestHttpRunner(unittest.TestCase): result = self.runner.get_summary() self.assertTrue(result.success) self.assertEqual(result.name, "request methods testcase: reference testcase") - self.assertEqual(result.step_datas[0].name, "request with variables") + self.assertEqual(result.step_datas[0].name, "request with functions") self.assertEqual(len(result.step_datas), 1) From c2ee26d5b570c659c809db8f518795966ebe35de Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 19 May 2020 10:42:16 +0800 Subject: [PATCH 12/20] change: testsuite pytest dirname --- .../request_with_functions_test.py | 4 ++-- .../request_with_testcase_reference_test.py | 4 ++-- httprunner/ext/make/__init__.py | 5 +++-- httprunner/ext/make/make_test.py | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) rename examples/postman_echo/request_methods/{demo_testsuite => demo_testsuite_yml}/request_with_functions_test.py (95%) rename examples/postman_echo/request_methods/{demo_testsuite => demo_testsuite_yml}/request_with_testcase_reference_test.py (84%) diff --git a/examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py similarity index 95% rename from examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py rename to examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py index aaec48f9..887c0d28 100644 --- a/examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py @@ -1,5 +1,5 @@ # NOTICE: Generated By HttpRunner. DO'NOT EDIT! -# FROM: examples/postman_echo/request_methods/demo_testsuite/request_with_functions.yml +# FROM: examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions.yml from httprunner import HttpRunner, TConfig, TStep @@ -10,7 +10,7 @@ class TestCaseRequestWithFunctions(HttpRunner): "variables": {"foo1": "session_bar1", "var1": "testsuite_val1"}, "base_url": "https://postman-echo.com", "verify": False, - "path": "examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py", + "path": "examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py", } ) diff --git a/examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py similarity index 84% rename from examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py rename to examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py index fae547cd..9a714755 100644 --- a/examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py @@ -1,5 +1,5 @@ # NOTICE: Generated By HttpRunner. DO'NOT EDIT! -# FROM: examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference.yml +# FROM: examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference.yml from httprunner import HttpRunner, TConfig, TStep @@ -10,7 +10,7 @@ class TestCaseRequestWithTestcaseReference(HttpRunner): "variables": {"foo1": "session_bar1", "var2": "testsuite_val2"}, "base_url": "https://postman-echo.com", "verify": False, - "path": "examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py", + "path": "examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py", } ) diff --git a/httprunner/ext/make/__init__.py b/httprunner/ext/make/__init__.py index a4df826a..466dddd1 100644 --- a/httprunner/ext/make/__init__.py +++ b/httprunner/ext/make/__init__.py @@ -134,7 +134,8 @@ def make_testsuite(testsuite: Dict) -> List[Text]: logger.info(f"start to make testsuite: {testsuite_path}") # create directory with testsuite file name, put its testcases under this directory - os.makedirs(os.path.dirname(os.path.splitext(testsuite_path)[0]), exist_ok=True) + testsuite_dir = testsuite_path.replace(".", "_") + os.makedirs(testsuite_dir, exist_ok=True) testcase_files = [] @@ -145,7 +146,7 @@ def make_testsuite(testsuite: Dict) -> List[Text]: testcase_dict = load_test_file(testcase_path) testcase_dict.setdefault("config", {}) testcase_dict["config"]["path"] = os.path.join( - os.path.splitext(testsuite_path)[0], os.path.basename(testcase_path) + testsuite_dir, os.path.basename(testcase_path) ) # override testcase name diff --git a/httprunner/ext/make/make_test.py b/httprunner/ext/make/make_test.py index 7de86252..7145807c 100644 --- a/httprunner/ext/make/make_test.py +++ b/httprunner/ext/make/make_test.py @@ -59,10 +59,10 @@ class TestLoader(unittest.TestCase): testcase_python_list = main_make(path) self.assertEqual(len(testcase_python_list), 2) self.assertIn( - "examples/postman_echo/request_methods/demo_testsuite/request_with_functions_test.py", + "examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py", testcase_python_list, ) self.assertIn( - "examples/postman_echo/request_methods/demo_testsuite/request_with_testcase_reference_test.py", + "examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py", testcase_python_list, ) From 7ef1ca608c9a3f2469b0bb252ada2d29aa4c6aee Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 19 May 2020 11:02:18 +0800 Subject: [PATCH 13/20] change: format pytest case after all testcases generated --- httprunner/cli.py | 3 ++- httprunner/ext/make/__init__.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/httprunner/cli.py b/httprunner/cli.py index 68f67b21..f5f5655f 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -31,7 +31,8 @@ def main_run(extra_args): if len(tests_path_list) == 0: # has not specified any testcase path - raise exceptions.ParamsError("Missed testcase path") + logger.error(f"No valid testcase path in cli arguments: {extra_args}") + sys.exit(1) testcase_path_list = main_make(tests_path_list) if not testcase_path_list: diff --git a/httprunner/ext/make/__init__.py b/httprunner/ext/make/__init__.py index 466dddd1..b4652553 100644 --- a/httprunner/ext/make/__init__.py +++ b/httprunner/ext/make/__init__.py @@ -59,10 +59,10 @@ def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]: return testcase_python_path, name_in_title_case -def format_pytest_with_black(python_path: Text): - logger.info(f"format pytest case with black: {python_path}") +def format_pytest_with_black(python_paths: List[Text]): + logger.info("format pytest case with black ...") try: - subprocess.run(["black", python_path]) + subprocess.run(["black", *python_paths]) except subprocess.CalledProcessError as ex: logger.error(ex) @@ -106,7 +106,6 @@ def make_testcase(testcase: Dict) -> Union[str, None]: f.write(content) logger.info(f"generated testcase: {testcase_python_path}") - format_pytest_with_black(testcase_python_path) return testcase_python_path @@ -222,6 +221,7 @@ def main_make(tests_paths: List[Text]) -> List: for tests_path in tests_paths: testcase_path_list.extend(__make(tests_path)) + format_pytest_with_black(testcase_path_list) return testcase_path_list From c13598d2af724fca006b101a940962d522209c6b Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 19 May 2020 11:07:57 +0800 Subject: [PATCH 14/20] docs: update changelog --- docs/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 39e42075..2166c431 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,10 +1,11 @@ # Release History -## 3.0.4 (2020-05-18) +## 3.0.4 (2020-05-19) **Added** - feat: make testsuite and run testsuite +- feat: testcase/testsuite config support getting variables by function **Fixed** From cb62ad0f9345a2fad9938cda7c8fddb3a25d13f4 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 19 May 2020 11:56:39 +0800 Subject: [PATCH 15/20] feat: har2case with request cookies --- docs/CHANGELOG.md | 1 + httprunner/ext/har2case/core.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2166c431..4d6ebe1b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,7 @@ - feat: make testsuite and run testsuite - feat: testcase/testsuite config support getting variables by function +- feat: har2case with request cookies **Fixed** diff --git a/httprunner/ext/har2case/core.py b/httprunner/ext/har2case/core.py index f7683eac..a65b2bfd 100644 --- a/httprunner/ext/har2case/core.py +++ b/httprunner/ext/har2case/core.py @@ -93,6 +93,14 @@ class HarParser(object): teststep_dict["request"]["method"] = method + def __make_request_cookies(self, teststep_dict, entry_json): + cookies = {} + for cookie in entry_json["request"].get("cookies", []): + cookies[cookie["name"]] = cookie["value"] + + if cookies: + teststep_dict["request"]["cookies"] = cookies + def __make_request_headers(self, teststep_dict, entry_json): """ parse HAR entry request headers, and make teststep headers. header in IGNORE_REQUEST_HEADERS will be ignored. @@ -288,6 +296,7 @@ class HarParser(object): self.__make_request_url(teststep_dict, entry_json) self.__make_request_method(teststep_dict, entry_json) + self.__make_request_cookies(teststep_dict, entry_json) self.__make_request_headers(teststep_dict, entry_json) self._make_request_data(teststep_dict, entry_json) self._make_validate(teststep_dict, entry_json) From 695e313aba0da2edb888b70b8f73a9b7703138b1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 19 May 2020 11:59:25 +0800 Subject: [PATCH 16/20] change: har2case do not ignore request headers --- httprunner/ext/har2case/core.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/httprunner/ext/har2case/core.py b/httprunner/ext/har2case/core.py index a65b2bfd..e460169f 100644 --- a/httprunner/ext/har2case/core.py +++ b/httprunner/ext/har2case/core.py @@ -14,24 +14,6 @@ except ImportError: JSONDecodeError = ValueError -IGNORE_REQUEST_HEADERS = [ - "host", - "accept", - "content-length", - "connection", - "accept-encoding", - "accept-language", - "origin", - "cache-control", - "pragma", - "upgrade-insecure-requests", - ":authority", - ":method", - ":scheme", - ":path", -] - - class HarParser(object): def __init__(self, har_file_path, filter_str=None, exclude_str=None): self.har_file_path = har_file_path @@ -127,9 +109,6 @@ class HarParser(object): """ teststep_headers = {} for header in entry_json["request"].get("headers", []): - if header["name"].lower() in IGNORE_REQUEST_HEADERS: - continue - teststep_headers[header["name"]] = header["value"] if teststep_headers: From f0b03d09ab491c38e61f53cd1127a23a374ed14b Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 19 May 2020 12:56:18 +0800 Subject: [PATCH 17/20] fix: remove cookie in request headers --- httprunner/ext/har2case/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/httprunner/ext/har2case/core.py b/httprunner/ext/har2case/core.py index e460169f..c747b3c7 100644 --- a/httprunner/ext/har2case/core.py +++ b/httprunner/ext/har2case/core.py @@ -109,6 +109,9 @@ class HarParser(object): """ teststep_headers = {} for header in entry_json["request"].get("headers", []): + if header["name"] == "cookie": + continue + teststep_headers[header["name"]] = header["value"] if teststep_headers: From 8b0f31bd244cafa8a4a025b7cf1e114412c1e7d8 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 19 May 2020 13:00:14 +0800 Subject: [PATCH 18/20] fix: remove request header startswith : e.g. :method, :scheme --- docs/CHANGELOG.md | 4 ++++ httprunner/ext/har2case/core.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4d6ebe1b..ac46eb4f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,10 @@ - fix: extract response cookies - fix: handle errors when no valid testcases generated +**Changed** + +- change: har2case do not ignore request headers, except for header startswith : + ## 3.0.3 (2020-05-17) **Fixed** diff --git a/httprunner/ext/har2case/core.py b/httprunner/ext/har2case/core.py index c747b3c7..3130a91c 100644 --- a/httprunner/ext/har2case/core.py +++ b/httprunner/ext/har2case/core.py @@ -109,7 +109,7 @@ class HarParser(object): """ teststep_headers = {} for header in entry_json["request"].get("headers", []): - if header["name"] == "cookie": + if header["name"] == "cookie" or header["name"].startswith(":"): continue teststep_headers[header["name"]] = header["value"] From d8ad62d7db94f649348d8427115bc2a4a320a266 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 19 May 2020 14:26:46 +0800 Subject: [PATCH 19/20] bump version to 3.0.4 --- httprunner/__init__.py | 2 +- httprunner/client.py | 4 ---- httprunner/ext/make/__init__.py | 2 +- httprunner/response.py | 2 ++ httprunner/runner.py | 7 ++----- pyproject.toml | 2 +- 6 files changed, 7 insertions(+), 12 deletions(-) diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 2b0bf6b7..1c9ba758 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.0.3" +__version__ = "3.0.4" __description__ = "One-stop solution for HTTP(S) testing." from httprunner.runner import HttpRunner diff --git a/httprunner/client.py b/httprunner/client.py index b9606e3f..22ccb8b1 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -195,10 +195,6 @@ class HttpSession(requests.Session): Safe mode has been removed from requests 1.x. """ try: - msg = "processed request:\n" - msg += f"> {method} {url}\n" - msg += f"> kwargs: {kwargs}" - logger.debug(msg) return requests.Session.request(self, method, url, **kwargs) except (MissingSchema, InvalidSchema, InvalidURL): raise diff --git a/httprunner/ext/make/__init__.py b/httprunner/ext/make/__init__.py index b4652553..80dad535 100644 --- a/httprunner/ext/make/__init__.py +++ b/httprunner/ext/make/__init__.py @@ -60,7 +60,7 @@ def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]: def format_pytest_with_black(python_paths: List[Text]): - logger.info("format pytest case with black ...") + logger.info("format pytest cases with black ...") try: subprocess.run(["black", *python_paths]) except subprocess.CalledProcessError as ex: diff --git a/httprunner/response.py b/httprunner/response.py index b739da95..bc1f6891 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -164,6 +164,8 @@ class ResponseObject(object): # check item check_item = u_validator["check"] + # TODO: validate variable or function + # check_item = parse_data(check_item, variables_mapping, functions_mapping) check_value = jmespath.search(check_item, self.resp_obj_meta) check_value = parse_string_value(check_value) diff --git a/httprunner/runner.py b/httprunner/runner.py index 7095c145..df2824f5 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -65,12 +65,8 @@ class HttpRunner(object): method = parsed_request_dict.pop("method") url_path = parsed_request_dict.pop("url") url = build_url(self.config.base_url, url_path) - parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {}) - logger.info(f"{method} {url}") - logger.debug(f"request kwargs(raw): {parsed_request_dict}") - # request self.__session = self.__session or HttpSession() resp = self.__session.request(method, url, **parsed_request_dict) @@ -148,7 +144,7 @@ class HttpRunner(object): def __run_step(self, step: TStep): """run teststep, teststep maybe a request or referenced testcase""" - logger.info(f"run step: {step.name}") + logger.info(f"run step begin: {step.name} >>>>>>") if step.request: step_data = self.__run_step_request(step) @@ -160,6 +156,7 @@ class HttpRunner(object): ) self.__step_datas.append(step_data) + logger.info(f"run step end: {step.name} <<<<<<\n") return step_data.export def run(self, testcase: TestCase): diff --git a/pyproject.toml b/pyproject.toml index ce3b3c40..6614464f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "3.0.3" +version = "3.0.4" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 7158128fef7e5193948a2b435d448d79c72f4df9 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 19 May 2020 16:07:14 +0800 Subject: [PATCH 20/20] feat: log request/response headers and body with indent --- docs/CHANGELOG.md | 1 + httprunner/client.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ac46eb4f..2e794dd4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,7 @@ - feat: make testsuite and run testsuite - feat: testcase/testsuite config support getting variables by function - feat: har2case with request cookies +- feat: log request/response headers and body with indent **Fixed** diff --git a/httprunner/client.py b/httprunner/client.py index 22ccb8b1..b7e645a7 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -1,3 +1,4 @@ +import json import time import requests @@ -32,12 +33,19 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData: def log_print(req_or_resp, r_type): msg = f"\n================== {r_type} details ==================\n" for key, value in req_or_resp.dict().items(): - msg += "{:<16} : {}\n".format(key, repr(value)) + if isinstance(value, dict): + value = json.dumps(value, indent=4) + + msg += "{:<8} : {}\n".format(key, value) logger.debug(msg) # record actual request info request_headers = dict(resp_obj.request.headers) request_body = resp_obj.request.body + try: + request_body = json.loads(request_body) + except json.JSONDecodeError: + pass if request_body: request_content_type = lower_dict_keys(request_headers).get("content-type")