From 365996f59b93f37dd9f8232f8dfa9c6dd1e9b1da Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 16:46:02 +0800 Subject: [PATCH 01/32] fix compatibility: step_datas=>records --- httprunner/compat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/httprunner/compat.py b/httprunner/compat.py index d34c0001..2d7fd593 100644 --- a/httprunner/compat.py +++ b/httprunner/compat.py @@ -265,7 +265,9 @@ def session_fixture(request): ) summary["stat"]["teststeps"]["failures"] += 1 - summary["details"].append(testcase_summary.dict()) + testcase_summary_json = testcase_summary.dict() + testcase_summary_json["records"] = testcase_summary_json.pop("step_datas") + summary["details"].append(testcase_summary_json) summary_path = "{{SUMMARY_PATH_PLACEHOLDER}}" summary_dir = os.path.dirname(summary_path) From f92fca70f2dd8559ee00ee60cd624bc77c315e96 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 16:47:26 +0800 Subject: [PATCH 02/32] fix: missing exit code from pytest --- httprunner/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httprunner/cli.py b/httprunner/cli.py index c86cca75..5297074d 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -44,7 +44,7 @@ def main_run(extra_args): sys.exit(1) extra_args_new.extend(testcase_path_list) - pytest.main(extra_args_new) + sys.exit(pytest.main(extra_args_new)) def main(): From e22d828dca3e726f9d58f9b1d1b558b25ec50dcb Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 15:33:42 +0800 Subject: [PATCH 03/32] docs: update sponsor info --- README.md | 6 +++--- docs/assets/hogwarts.png | Bin 63747 -> 9278 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7ba2bf44..860e1b60 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,11 @@ Thank you to all our sponsors! ✨🍰✨ ([become a sponsor](docs/sponsors.md)) ### 金牌赞助商(Gold Sponsor) -[霍格沃兹测试学院](https://testing-studio.com) +[霍格沃兹测试学院](https://ceshiren.com/) -> [霍格沃兹测试学院](https://testing-studio.com) 是由测吧(北京)科技有限公司与知名软件测试社区 [TesterHome](https://testerhome.com/) 合作的高端教育品牌。由 BAT 一线**测试大咖执教**,提供**实战驱动**的接口自动化测试、移动自动化测试、性能测试、持续集成与 DevOps 等技术培训,以及测试开发优秀人才内推服务。[点击学习!](https://ke.qq.com/course/254956?flowToken=1014690) +> [霍格沃兹测试学院](https://ceshiren.com/) 是业界领先的测试开发技术高端教育品牌,隶属于测吧(北京)科技有限公司。学院课程均由 BAT 一线测试大咖执教,提供实战驱动的接口自动化测试、移动自动化测试、性能测试、持续集成与 DevOps 等技术培训,以及测试开发优秀人才内推服务。[点击学习!](https://ke.qq.com/course/254956?flowToken=1014690) -霍格沃兹测试学院是 HttpRunner 的首家金牌赞助商。 +[霍格沃兹测试学院](https://ceshiren.com/) 是 HttpRunner 的首家金牌赞助商。 ### 开源服务赞助商(Open Source Sponsor) diff --git a/docs/assets/hogwarts.png b/docs/assets/hogwarts.png index 5c111d6458a4675fb1586f4cd7b049b0c770df90..2f8c752ce9838b27ea57f8c7565114714847d019 100644 GIT binary patch literal 9278 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 literal 63747 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Ξ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 NBt>OKs)Y3X{|A=JWJ>@5 From 47cc964ecfd967fc85ff02fa9a8d080a217e75fa Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 31 May 2020 10:53:43 +0800 Subject: [PATCH 04/32] fix: convert jmespath.search result to int/float --- docs/CHANGELOG.md | 6 ++++++ .../request_methods/demo_testsuite_yml/__init__.py | 1 + .../demo_testsuite_yml/request_with_functions_test.py | 2 +- .../postman_echo/request_methods/request_with_functions.yml | 2 +- .../request_methods/request_with_functions_test.py | 2 +- .../request_methods/validate_with_functions.yml | 4 ++-- .../request_methods/validate_with_functions_test.py | 3 +-- httprunner/response.py | 3 +-- 8 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 examples/postman_echo/request_methods/demo_testsuite_yml/__init__.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9c736212..71f6e0c9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 3.0.7 (2020-05-31) + +**Fixed** + +- fix: convert jmespath.search result to int/float + ## 3.0.6 (2020-05-29) **Added** diff --git a/examples/postman_echo/request_methods/demo_testsuite_yml/__init__.py b/examples/postman_echo/request_methods/demo_testsuite_yml/__init__.py new file mode 100644 index 00000000..bed305d5 --- /dev/null +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! \ No newline at end of file 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 5c8647f8..0bc92c4d 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 @@ -34,7 +34,7 @@ class TestCaseRequestWithFunctions(HttpRunner): "validate": [ {"eq": ["status_code", 200]}, {"eq": ["body.args.foo1", "session_bar1"]}, - {"eq": ["body.args.sum_v", 3]}, + {"eq": ["body.args.sum_v", "3"]}, {"eq": ["body.args.foo2", "session_bar2"]}, ], } diff --git a/examples/postman_echo/request_methods/request_with_functions.yml b/examples/postman_echo/request_methods/request_with_functions.yml index 66a94ba4..6fc68325 100644 --- a/examples/postman_echo/request_methods/request_with_functions.yml +++ b/examples/postman_echo/request_methods/request_with_functions.yml @@ -26,7 +26,7 @@ teststeps: validate: - eq: ["status_code", 200] - eq: ["body.args.foo1", "session_bar1"] - - eq: ["body.args.sum_v", 3] + - eq: ["body.args.sum_v", "3"] - eq: ["body.args.foo2", "session_bar2"] - name: post raw text 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 504b895a..b8f333bb 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -34,7 +34,7 @@ class TestCaseRequestWithFunctions(HttpRunner): "validate": [ {"eq": ["status_code", 200]}, {"eq": ["body.args.foo1", "session_bar1"]}, - {"eq": ["body.args.sum_v", 3]}, + {"eq": ["body.args.sum_v", "3"]}, {"eq": ["body.args.foo2", "session_bar2"]}, ], } diff --git a/examples/postman_echo/request_methods/validate_with_functions.yml b/examples/postman_echo/request_methods/validate_with_functions.yml index 41aca935..4d8d8a83 100644 --- a/examples/postman_echo/request_methods/validate_with_functions.yml +++ b/examples/postman_echo/request_methods/validate_with_functions.yml @@ -25,5 +25,5 @@ teststeps: session_foo2: "body.args.foo2" validate: - eq: ["status_code", 200] - - eq: ["body.args.sum_v", 3] - - less_than: ["body.args.sum_v", "${sum_two(2, 2)}"] + - eq: ["body.args.sum_v", "3"] +# - less_than: ["body.args.sum_v", "${sum_two(2, 2)}"] TODO 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 51640531..1346ec8e 100644 --- a/examples/postman_echo/request_methods/validate_with_functions_test.py +++ b/examples/postman_echo/request_methods/validate_with_functions_test.py @@ -33,8 +33,7 @@ class TestCaseValidateWithFunctions(HttpRunner): "extract": {"session_foo2": "body.args.foo2"}, "validate": [ {"eq": ["status_code", 200]}, - {"eq": ["body.args.sum_v", 3]}, - {"less_than": ["body.args.sum_v", "${sum_two(2, 2)}"]}, + {"eq": ["body.args.sum_v", "3"]}, ], } ), diff --git a/httprunner/response.py b/httprunner/response.py index 8c698542..806d7f86 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -169,11 +169,10 @@ class ResponseObject(object): check_value = parse_data( check_item, variables_mapping, functions_mapping ) + check_value = parse_string_value(check_value) else: check_value = jmespath.search(check_item, self.resp_obj_meta) - check_value = parse_string_value(check_value) - # comparator assert_method = u_validator["assert"] assert_func = get_mapping_function(assert_method, functions_mapping) From f87aaf56624562bec94c434c718d73b9aac6a297 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 31 May 2020 12:05:04 +0800 Subject: [PATCH 05/32] fix: referenced testcase should not be run duplicately --- httprunner/make.py | 15 ++++++++++----- tests/make_test.py | 8 ++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/httprunner/make.py b/httprunner/make.py index 749131dc..0d19aacd 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -132,7 +132,9 @@ def __format_pytest_with_black(python_paths: List[Text]) -> NoReturn: logger.error(ex) -def __make_testcase(testcase: Dict, dir_path: Text = None) -> NoReturn: +def __make_testcase( + testcase: Dict, dir_path: Text = None, ref_flag: bool = False +) -> NoReturn: """convert valid testcase dict to pytest file path""" # ensure compatibility with testcase format v2 testcase = ensure_testcase_v3(testcase) @@ -174,7 +176,7 @@ def __make_testcase(testcase: Dict, dir_path: Text = None) -> NoReturn: # make ref testcase pytest file ref_testcase_path = __ensure_absolute(teststep["testcase"]) - __make(ref_testcase_path) + __make(ref_testcase_path, ref_flag=True) # prepare ref testcase class name ref_testcase_python_path, ref_testcase_cls_name = convert_testcase_path( @@ -206,7 +208,9 @@ def __make_testcase(testcase: Dict, dir_path: Text = None) -> NoReturn: __ensure_testcase_module(testcase_python_path) logger.info(f"generated testcase: {testcase_python_path}") - make_files_cache_set.add(__ensure_cwd_relative(testcase_python_path)) + + if not ref_flag: + make_files_cache_set.add(__ensure_cwd_relative(testcase_python_path)) def __make_testsuite(testsuite: Dict) -> NoReturn: @@ -257,12 +261,13 @@ def __make_testsuite(testsuite: Dict) -> NoReturn: __make_testcase(testcase_dict, testsuite_dir) -def __make(tests_path: Text) -> NoReturn: +def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn: """ make testcase(s) with testcase/testsuite/folder absolute path generated pytest file path will be cached in make_files_cache_set Args: tests_path: should be in absolute path + ref_flag: flag if referenced test path """ test_files = [] @@ -290,7 +295,7 @@ def __make(tests_path: Text) -> NoReturn: # testcase if "teststeps" in test_content: try: - __make_testcase(test_content) + __make_testcase(test_content, ref_flag=ref_flag) except exceptions.TestCaseFormatError: continue diff --git a/tests/make_test.py b/tests/make_test.py index cabb27ed..7884910e 100644 --- a/tests/make_test.py +++ b/tests/make_test.py @@ -18,7 +18,7 @@ class TestLoader(unittest.TestCase): ] make_files_cache_set.clear() testcase_python_list = main_make(path) - self.assertEqual(len(testcase_python_list), 2) + self.assertEqual(len(testcase_python_list), 1) self.assertIn( "examples/postman_echo/request_methods/request_with_testcase_reference_test.py", testcase_python_list, @@ -86,7 +86,7 @@ from examples.postman_echo.request_methods.request_with_functions_test import ( path = ["examples/postman_echo/request_methods/demo_testsuite.yml"] make_files_cache_set.clear() testcase_python_list = main_make(path) - self.assertEqual(len(testcase_python_list), 3) + self.assertEqual(len(testcase_python_list), 2) self.assertIn( "examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py", testcase_python_list, @@ -95,7 +95,3 @@ from examples.postman_echo.request_methods.request_with_functions_test import ( "examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py", testcase_python_list, ) - self.assertIn( - "examples/postman_echo/request_methods/request_with_functions_test.py", - testcase_python_list, - ) From 56aa8f251f1a4186fdfa1e1b542a19a35d86c38f Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 31 May 2020 14:23:05 +0800 Subject: [PATCH 06/32] fix: requests.cookies.CookieConflictError, multiple cookies with name --- httprunner/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httprunner/client.py b/httprunner/client.py index e1ade62a..e6116bb7 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -41,7 +41,7 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData: # record actual request info request_headers = dict(resp_obj.request.headers) - request_cookies = dict(resp_obj.request._cookies) + request_cookies = resp_obj.request._cookies.get_dict() request_body = resp_obj.request.body try: request_body = json.loads(request_body) From edf17c550877322eef6e885ba8428ba4028a23ef Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 11:06:48 +0800 Subject: [PATCH 07/32] docs: update changelog --- docs/CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 71f6e0c9..5bb94260 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,10 +1,12 @@ # Release History -## 3.0.7 (2020-05-31) +## 3.0.7 (2020-06-01) **Fixed** -- fix: convert jmespath.search result to int/float +- fix: convert jmespath.search result to int/float unintentionally +- fix: referenced testcase should not be run duplicately +- fix: requests.cookies.CookieConflictError, multiple cookies with name ## 3.0.6 (2020-05-29) From e7aca434700d7572ac58f012ca9d01a52ad379c4 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 11:25:16 +0800 Subject: [PATCH 08/32] feat: describe testcase in chain-call style --- .../request_with_functions_test.py | 141 ++++++++---------- httprunner/__init__.py | 7 +- httprunner/runner.py | 116 ++++++++++++++ httprunner/schema.py | 4 +- pyproject.toml | 2 +- 5 files changed, 189 insertions(+), 81 deletions(-) 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 b8f333bb..78a4ac4f 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -1,88 +1,77 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/postman_echo/request_methods/request_with_functions.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, Request class TestCaseRequestWithFunctions(HttpRunner): - config = TConfig( - **{ - "name": "request methods testcase with functions", - "variables": {"foo1": "session_bar1"}, - "base_url": "https://postman-echo.com", - "verify": False, - "path": "examples/postman_echo/request_methods/request_with_functions_test.py", - } + config = ( + Config("request methods testcase with functions") + .set_variables(foo1="session_bar1") + .set_base_url("https://postman-echo.com") + .set_verify(False) + .set_path( + "examples/postman_echo/request_methods/request_with_functions_test.py" + ) + .init() ) 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"]}, - ], - } - ), + Step("get with params") + .set_variables(foo1="bar1", foo2="session_bar2", sum_v="${sum_two(1, 2)}") + .run_request( + Request() + .set_method("GET") + .set_url("/get") + .set_params(foo1="$foo1", foo2="$foo2", sum_v="$sum_v") + .set_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) + ) + .extract("session_foo2", "body.args.foo2") + .assert_equal("status_code", 200) + .assert_equal("body.args.foo1", "session_bar1") + .assert_equal("body.args.sum_v", "3") + .assert_equal("body.args.foo2", "session_bar2") + .init(), + Step("post raw text") + .set_variables(foo1="hello world", foo3="$session_foo2") + .run_request( + Request() + .set_method("POST") + .set_url("/post") + .set_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "text/plain", + } + ) + .set_data( + "This is expected to be sent back as part of response body: $foo1-$foo3." + ) + ) + .assert_equal("status_code", 200) + .assert_equal( + "body.data", + "This is expected to be sent back as part of response body: session_bar1-session_bar2.", + ) + .init(), + Step("post form data") + .set_variables(**{"foo1": "bar1", "foo2": "bar2"}) + .run_request( + Request() + .set_method("POST") + .set_url("/post") + .set_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .set_data("foo1=$foo1&foo2=$foo2") + ) + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "session_bar1") + .assert_equal("body.form.foo2", "bar2") + .init(), ] diff --git a/httprunner/__init__.py b/httprunner/__init__.py index d6cc44a1..2c1e9ef4 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,7 +1,7 @@ -__version__ = "3.0.6" +__version__ = "3.0.7" __description__ = "One-stop solution for HTTP(S) testing." -from httprunner.runner import HttpRunner +from httprunner.runner import HttpRunner, Config, Step, Request from httprunner.schema import TConfig, TStep __all__ = [ @@ -10,4 +10,7 @@ __all__ = [ "HttpRunner", "TConfig", "TStep", + "Config", + "Step", + "Request", ] diff --git a/httprunner/runner.py b/httprunner/runner.py index d236d471..a658e561 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -30,9 +30,125 @@ from httprunner.schema import ( TestCaseInOut, ProjectMeta, TestCase, + TRequest, ) +class Config(object): + def __init__(self, name): + self.__name = name + self.__variables = {} + self.__base_url = "" + self.__verify = False + self.__path = "" + + def set_variables(self, **variables): + self.__variables.update(variables) + return self + + def set_base_url(self, base_url): + self.__base_url = base_url + return self + + def set_verify(self, verify): + self.__verify = verify + return self + + def set_path(self, path): + self.__path = path + return self + + def init(self): + return TConfig( + name=self.__name, + base_url=self.__base_url, + verify=self.__verify, + variables=self.__variables, + path=self.__path, + ) + + +class Request(object): + def __init__(self): + self.__method = "GET" + self.__url = "" + self.__params = {} + self.__headers = {} + self.__data = "" + + def set_method(self, method): + self.__method = method + return self + + def set_url(self, url): + self.__url = url + return self + + def set_params(self, **params): + self.__params.update(params) + return self + + def set_headers(self, **headers): + self.__headers.update(headers) + return self + + def set_data(self, data): + self.__data = data + return self + + def perform(self): + """build TRequest object with configs""" + return TRequest( + method=self.__method, + url=self.__url, + params=self.__params, + headers=self.__headers, + data=self.__data, + ) + + +class Step(object): + def __init__(self, name): + self.__name = name + self.__variables = {} + self.__request = None + self.__extract = {} + self.__validators = [] + + def set_variables(self, **variables): + self.__variables.update(variables) + return self + + def extract(self, var_name, jmes_path): + self.__extract[var_name] = jmes_path + return self + + def assert_equal(self, jmes_path, expected_value): + self.__validators.append({"eq": [jmes_path, expected_value]}) + return self + + def assert_greater_than(self, jmes_path, expected_value): + self.__validators.append({"gt": [jmes_path, expected_value]}) + return self + + def assert_less_than(self, jmes_path, expected_value): + self.__validators.append({"lt": [jmes_path, expected_value]}) + return self + + def run_request(self, req_obj: Request) -> "Step": + self.__request = req_obj.perform() + return self + + def init(self): + return TStep( + name=self.__name, + variables=self.__variables, + request=self.__request, + extract=self.__extract, + validate=self.__validators, + ) + + class HttpRunner(object): config: TConfig teststeps: List[TStep] diff --git a/httprunner/schema.py b/httprunner/schema.py index 986f6cb2..217d294d 100644 --- a/httprunner/schema.py +++ b/httprunner/schema.py @@ -45,7 +45,7 @@ class TConfig(BaseModel): path: Text = None -class Request(BaseModel): +class TRequest(BaseModel): """requests.Request model""" method: MethodEnum = MethodEnum.GET @@ -63,7 +63,7 @@ class Request(BaseModel): class TStep(BaseModel): name: Name - request: Request = None + request: TRequest = None testcase: Union[Text, Callable] = "" variables: VariablesMapping = {} setup_hooks: Hook = [] diff --git a/pyproject.toml b/pyproject.toml index ec8c3459..083c1725 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "3.0.6" +version = "3.0.7" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 526b1fcbc921a8703cf9a095d0ca80a264d8cb7f Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 11:56:16 +0800 Subject: [PATCH 09/32] change: refactor Request interface --- .../request_with_functions_test.py | 9 ++--- httprunner/runner.py | 38 +++++++++++++------ httprunner/schema.py | 2 - 3 files changed, 30 insertions(+), 19 deletions(-) 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 78a4ac4f..1cf03711 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -21,8 +21,7 @@ class TestCaseRequestWithFunctions(HttpRunner): .set_variables(foo1="bar1", foo2="session_bar2", sum_v="${sum_two(1, 2)}") .run_request( Request() - .set_method("GET") - .set_url("/get") + .get("/get") .set_params(foo1="$foo1", foo2="$foo2", sum_v="$sum_v") .set_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) ) @@ -36,8 +35,7 @@ class TestCaseRequestWithFunctions(HttpRunner): .set_variables(foo1="hello world", foo3="$session_foo2") .run_request( Request() - .set_method("POST") - .set_url("/post") + .post("/post") .set_headers( **{ "User-Agent": "HttpRunner/${get_httprunner_version()}", @@ -58,8 +56,7 @@ class TestCaseRequestWithFunctions(HttpRunner): .set_variables(**{"foo1": "bar1", "foo2": "bar2"}) .run_request( Request() - .set_method("POST") - .set_url("/post") + .post("/post") .set_headers( **{ "User-Agent": "HttpRunner/${get_httprunner_version()}", diff --git a/httprunner/runner.py b/httprunner/runner.py index a658e561..e2fb9413 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -31,6 +31,7 @@ from httprunner.schema import ( ProjectMeta, TestCase, TRequest, + MethodEnum, ) @@ -69,21 +70,36 @@ class Config(object): class Request(object): - def __init__(self): - self.__method = "GET" - self.__url = "" + def get(self, url): + return RequestOptionalArgs(MethodEnum.GET, url) + + def post(self, url): + return RequestOptionalArgs(MethodEnum.POST, url) + + def put(self, url): + return RequestOptionalArgs(MethodEnum.PUT, url) + + def head(self, url): + return RequestOptionalArgs(MethodEnum.HEAD, url) + + def delete(self, url): + return RequestOptionalArgs(MethodEnum.DELETE, url) + + def options(self, url): + return RequestOptionalArgs(MethodEnum.OPTIONS, url) + + def patch(self, url): + return RequestOptionalArgs(MethodEnum.PATCH, url) + + +class RequestOptionalArgs(object): + def __init__(self, method: MethodEnum, url: Text): + self.__method = method + self.__url = url self.__params = {} self.__headers = {} self.__data = "" - def set_method(self, method): - self.__method = method - return self - - def set_url(self, url): - self.__url = url - return self - def set_params(self, **params): self.__params.update(params) return self diff --git a/httprunner/schema.py b/httprunner/schema.py index 217d294d..60b88cd7 100644 --- a/httprunner/schema.py +++ b/httprunner/schema.py @@ -29,8 +29,6 @@ class MethodEnum(Text, Enum): HEAD = "HEAD" OPTIONS = "OPTIONS" PATCH = "PATCH" - CONNECT = "CONNECT" - TRACE = "TRACE" class TConfig(BaseModel): From 03c183607531aebcc9f3cc147bc8f8f8cf256949 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 13:14:30 +0800 Subject: [PATCH 10/32] style: add typing --- .../request_with_functions_test.py | 26 ++-- httprunner/runner.py | 141 ++++++++++-------- httprunner/schema.py | 4 +- 3 files changed, 97 insertions(+), 74 deletions(-) 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 1cf03711..c8fad6a6 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -7,10 +7,10 @@ from httprunner import HttpRunner, Config, Step, Request class TestCaseRequestWithFunctions(HttpRunner): config = ( Config("request methods testcase with functions") - .set_variables(foo1="session_bar1") - .set_base_url("https://postman-echo.com") - .set_verify(False) - .set_path( + .variables(foo1="session_bar1") + .base_url("https://postman-echo.com") + .verify(False) + .path( "examples/postman_echo/request_methods/request_with_functions_test.py" ) .init() @@ -18,12 +18,12 @@ class TestCaseRequestWithFunctions(HttpRunner): teststeps = [ Step("get with params") - .set_variables(foo1="bar1", foo2="session_bar2", sum_v="${sum_two(1, 2)}") + .with_variables(foo1="bar1", foo2="session_bar2", sum_v="${sum_two(1, 2)}") .run_request( Request() .get("/get") - .set_params(foo1="$foo1", foo2="$foo2", sum_v="$sum_v") - .set_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) + .with_params(foo1="$foo1", foo2="$foo2", sum_v="$sum_v") + .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) ) .extract("session_foo2", "body.args.foo2") .assert_equal("status_code", 200) @@ -32,17 +32,17 @@ class TestCaseRequestWithFunctions(HttpRunner): .assert_equal("body.args.foo2", "session_bar2") .init(), Step("post raw text") - .set_variables(foo1="hello world", foo3="$session_foo2") + .with_variables(foo1="hello world", foo3="$session_foo2") .run_request( Request() .post("/post") - .set_headers( + .with_headers( **{ "User-Agent": "HttpRunner/${get_httprunner_version()}", "Content-Type": "text/plain", } ) - .set_data( + .with_data( "This is expected to be sent back as part of response body: $foo1-$foo3." ) ) @@ -53,17 +53,17 @@ class TestCaseRequestWithFunctions(HttpRunner): ) .init(), Step("post form data") - .set_variables(**{"foo1": "bar1", "foo2": "bar2"}) + .with_variables(**{"foo1": "bar1", "foo2": "bar2"}) .run_request( Request() .post("/post") - .set_headers( + .with_headers( **{ "User-Agent": "HttpRunner/${get_httprunner_version()}", "Content-Type": "application/x-www-form-urlencoded", } ) - .set_data("foo1=$foo1&foo2=$foo2") + .with_data("foo1=$foo1&foo2=$foo2") ) .assert_equal("status_code", 200) .assert_equal("body.form.foo1", "session_bar1") diff --git a/httprunner/runner.py b/httprunner/runner.py index e2fb9413..5738e00c 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -2,7 +2,7 @@ import os import time import uuid from datetime import datetime -from typing import List, Dict, Text +from typing import List, Dict, Text, Any, NoReturn try: import allure @@ -36,30 +36,30 @@ from httprunner.schema import ( class Config(object): - def __init__(self, name): + def __init__(self, name: Text): self.__name = name self.__variables = {} self.__base_url = "" self.__verify = False self.__path = "" - def set_variables(self, **variables): + def variables(self, **variables) -> "Config": self.__variables.update(variables) return self - def set_base_url(self, base_url): + def base_url(self, base_url: Text) -> "Config": self.__base_url = base_url return self - def set_verify(self, verify): + def verify(self, verify: bool) -> "Config": self.__verify = verify return self - def set_path(self, path): + def path(self, path: Text) -> "Config": self.__path = path return self - def init(self): + def init(self) -> TConfig: return TConfig( name=self.__name, base_url=self.__base_url, @@ -69,50 +69,47 @@ class Config(object): ) -class Request(object): - def get(self, url): - return RequestOptionalArgs(MethodEnum.GET, url) - - def post(self, url): - return RequestOptionalArgs(MethodEnum.POST, url) - - def put(self, url): - return RequestOptionalArgs(MethodEnum.PUT, url) - - def head(self, url): - return RequestOptionalArgs(MethodEnum.HEAD, url) - - def delete(self, url): - return RequestOptionalArgs(MethodEnum.DELETE, url) - - def options(self, url): - return RequestOptionalArgs(MethodEnum.OPTIONS, url) - - def patch(self, url): - return RequestOptionalArgs(MethodEnum.PATCH, url) - - class RequestOptionalArgs(object): def __init__(self, method: MethodEnum, url: Text): self.__method = method self.__url = url self.__params = {} self.__headers = {} + self.__cookies = {} self.__data = "" + self.__timeout = 120 + self.__allow_redirects = True + self.__verify = False - def set_params(self, **params): + def with_params(self, **params) -> "RequestOptionalArgs": self.__params.update(params) return self - def set_headers(self, **headers): + def with_headers(self, **headers) -> "RequestOptionalArgs": self.__headers.update(headers) return self - def set_data(self, data): + def with_cookies(self, **cookies) -> "RequestOptionalArgs": + self.__cookies.update(cookies) + return self + + def with_data(self, data) -> "RequestOptionalArgs": self.__data = data return self - def perform(self): + def set_timeout(self, timeout: float) -> "RequestOptionalArgs": + self.__timeout = timeout + return self + + def set_verify(self, verify: bool) -> "RequestOptionalArgs": + self.__verify = verify + return self + + def set_allow_redirects(self, allow_redirects: bool) -> "RequestOptionalArgs": + self.__allow_redirects = allow_redirects + return self + + def perform(self) -> TRequest: """build TRequest object with configs""" return TRequest( method=self.__method, @@ -120,9 +117,35 @@ class RequestOptionalArgs(object): params=self.__params, headers=self.__headers, data=self.__data, + timeout=self.__timeout, + verify=self.__verify, + allow_redirects=self.__allow_redirects, ) +class Request(object): + def get(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.GET, url) + + def post(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.POST, url) + + def put(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.PUT, url) + + def head(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.HEAD, url) + + def delete(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.DELETE, url) + + def options(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.OPTIONS, url) + + def patch(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.PATCH, url) + + class Step(object): def __init__(self, name): self.__name = name @@ -131,31 +154,31 @@ class Step(object): self.__extract = {} self.__validators = [] - def set_variables(self, **variables): + def with_variables(self, **variables) -> "Step": self.__variables.update(variables) return self - def extract(self, var_name, jmes_path): - self.__extract[var_name] = jmes_path - return self - - def assert_equal(self, jmes_path, expected_value): - self.__validators.append({"eq": [jmes_path, expected_value]}) - return self - - def assert_greater_than(self, jmes_path, expected_value): - self.__validators.append({"gt": [jmes_path, expected_value]}) - return self - - def assert_less_than(self, jmes_path, expected_value): - self.__validators.append({"lt": [jmes_path, expected_value]}) - return self - - def run_request(self, req_obj: Request) -> "Step": + def run_request(self, req_obj: RequestOptionalArgs) -> "Step": self.__request = req_obj.perform() return self - def init(self): + def extract(self, var_name: Text, jmes_path: Text) -> "Step": + self.__extract[var_name] = jmes_path + return self + + def assert_equal(self, jmes_path: Text, expected_value: Any) -> "Step": + self.__validators.append({"eq": [jmes_path, expected_value]}) + return self + + def assert_greater_than(self, jmes_path: Text, expected_value: Any) -> "Step": + self.__validators.append({"gt": [jmes_path, expected_value]}) + return self + + def assert_less_than(self, jmes_path: Text, expected_value: Any) -> "Step": + self.__validators.append({"lt": [jmes_path, expected_value]}) + return self + + def init(self) -> TStep: return TStep( name=self.__name, variables=self.__variables, @@ -197,7 +220,7 @@ class HttpRunner(object): self.__session_variables = variables return self - def __run_step_request(self, step: TStep): + def __run_step_request(self, step: TStep) -> StepData: """run teststep: request""" step_data = StepData(name=step.name) @@ -274,7 +297,7 @@ class HttpRunner(object): return step_data - def __run_step_testcase(self, step): + def __run_step_testcase(self, step: TStep) -> StepData: """run teststep: referenced testcase""" step_data = StepData(name=step.name) step_variables = step.variables @@ -315,7 +338,7 @@ class HttpRunner(object): return step_data - def __run_step(self, step: TStep): + def __run_step(self, step: TStep) -> Dict: """run teststep, teststep maybe a request or referenced testcase""" logger.info(f"run step begin: {step.name} >>>>>>") @@ -332,7 +355,7 @@ class HttpRunner(object): logger.info(f"run step end: {step.name} <<<<<<\n") return step_data.export - def __parse_config(self, config: TConfig): + def __parse_config(self, config: TConfig) -> NoReturn: config.variables.update(self.__session_variables) config.variables = parse_variables_mapping( config.variables, self.__project_meta.functions @@ -344,7 +367,7 @@ class HttpRunner(object): config.base_url, config.variables, self.__project_meta.functions ) - def run_testcase(self, testcase: TestCase): + def run_testcase(self, testcase: TestCase) -> "HttpRunner": """run specified testcase Examples: @@ -437,7 +460,7 @@ class HttpRunner(object): step_datas=self.__step_datas, ) - def test_start(self): + def test_start(self) -> "HttpRunner": """main entrance, discovered by pytest""" self.__project_meta = self.__project_meta or load_project_meta(self.config.path) self.__case_id = self.__case_id or str(uuid.uuid4()) diff --git a/httprunner/schema.py b/httprunner/schema.py index 60b88cd7..7599b8c6 100644 --- a/httprunner/schema.py +++ b/httprunner/schema.py @@ -46,14 +46,14 @@ class TConfig(BaseModel): class TRequest(BaseModel): """requests.Request model""" - method: MethodEnum = MethodEnum.GET + method: MethodEnum url: Url params: Dict[Text, Text] = {} headers: Headers = {} req_json: Dict = Field({}, alias="json") data: Union[Text, Dict[Text, Any]] = "" cookies: Cookies = {} - timeout: int = 120 + timeout: float = 120 allow_redirects: bool = True verify: Verify = False upload: Dict = {} # used for upload files From 0905350e99ca5700ae37428e7c8d43a28e2e8307 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 13:17:44 +0800 Subject: [PATCH 11/32] refactor: relocate testcase definitions --- httprunner/__init__.py | 3 +- httprunner/runner.py | 157 +-------------------------------------- httprunner/testcase.py | 162 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 157 deletions(-) create mode 100644 httprunner/testcase.py diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 2c1e9ef4..58134ded 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,8 +1,9 @@ __version__ = "3.0.7" __description__ = "One-stop solution for HTTP(S) testing." -from httprunner.runner import HttpRunner, Config, Step, Request +from httprunner.runner import HttpRunner from httprunner.schema import TConfig, TStep +from httprunner.testcase import Config, Step, Request __all__ = [ "__version__", diff --git a/httprunner/runner.py b/httprunner/runner.py index 5738e00c..20663408 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -2,7 +2,7 @@ import os import time import uuid from datetime import datetime -from typing import List, Dict, Text, Any, NoReturn +from typing import List, Dict, Text, NoReturn try: import allure @@ -30,164 +30,9 @@ from httprunner.schema import ( TestCaseInOut, ProjectMeta, TestCase, - TRequest, - MethodEnum, ) -class Config(object): - def __init__(self, name: Text): - self.__name = name - self.__variables = {} - self.__base_url = "" - self.__verify = False - self.__path = "" - - def variables(self, **variables) -> "Config": - self.__variables.update(variables) - return self - - def base_url(self, base_url: Text) -> "Config": - self.__base_url = base_url - return self - - def verify(self, verify: bool) -> "Config": - self.__verify = verify - return self - - def path(self, path: Text) -> "Config": - self.__path = path - return self - - def init(self) -> TConfig: - return TConfig( - name=self.__name, - base_url=self.__base_url, - verify=self.__verify, - variables=self.__variables, - path=self.__path, - ) - - -class RequestOptionalArgs(object): - def __init__(self, method: MethodEnum, url: Text): - self.__method = method - self.__url = url - self.__params = {} - self.__headers = {} - self.__cookies = {} - self.__data = "" - self.__timeout = 120 - self.__allow_redirects = True - self.__verify = False - - def with_params(self, **params) -> "RequestOptionalArgs": - self.__params.update(params) - return self - - def with_headers(self, **headers) -> "RequestOptionalArgs": - self.__headers.update(headers) - return self - - def with_cookies(self, **cookies) -> "RequestOptionalArgs": - self.__cookies.update(cookies) - return self - - def with_data(self, data) -> "RequestOptionalArgs": - self.__data = data - return self - - def set_timeout(self, timeout: float) -> "RequestOptionalArgs": - self.__timeout = timeout - return self - - def set_verify(self, verify: bool) -> "RequestOptionalArgs": - self.__verify = verify - return self - - def set_allow_redirects(self, allow_redirects: bool) -> "RequestOptionalArgs": - self.__allow_redirects = allow_redirects - return self - - def perform(self) -> TRequest: - """build TRequest object with configs""" - return TRequest( - method=self.__method, - url=self.__url, - params=self.__params, - headers=self.__headers, - data=self.__data, - timeout=self.__timeout, - verify=self.__verify, - allow_redirects=self.__allow_redirects, - ) - - -class Request(object): - def get(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.GET, url) - - def post(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.POST, url) - - def put(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.PUT, url) - - def head(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.HEAD, url) - - def delete(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.DELETE, url) - - def options(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.OPTIONS, url) - - def patch(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.PATCH, url) - - -class Step(object): - def __init__(self, name): - self.__name = name - self.__variables = {} - self.__request = None - self.__extract = {} - self.__validators = [] - - def with_variables(self, **variables) -> "Step": - self.__variables.update(variables) - return self - - def run_request(self, req_obj: RequestOptionalArgs) -> "Step": - self.__request = req_obj.perform() - return self - - def extract(self, var_name: Text, jmes_path: Text) -> "Step": - self.__extract[var_name] = jmes_path - return self - - def assert_equal(self, jmes_path: Text, expected_value: Any) -> "Step": - self.__validators.append({"eq": [jmes_path, expected_value]}) - return self - - def assert_greater_than(self, jmes_path: Text, expected_value: Any) -> "Step": - self.__validators.append({"gt": [jmes_path, expected_value]}) - return self - - def assert_less_than(self, jmes_path: Text, expected_value: Any) -> "Step": - self.__validators.append({"lt": [jmes_path, expected_value]}) - return self - - def init(self) -> TStep: - return TStep( - name=self.__name, - variables=self.__variables, - request=self.__request, - extract=self.__extract, - validate=self.__validators, - ) - - class HttpRunner(object): config: TConfig teststeps: List[TStep] diff --git a/httprunner/testcase.py b/httprunner/testcase.py new file mode 100644 index 00000000..dafd4f67 --- /dev/null +++ b/httprunner/testcase.py @@ -0,0 +1,162 @@ +from typing import Text, Any + +from httprunner.schema import ( + TConfig, + TStep, + TRequest, + MethodEnum, +) + + +class Config(object): + def __init__(self, name: Text): + self.__name = name + self.__variables = {} + self.__base_url = "" + self.__verify = False + self.__path = "" + + def variables(self, **variables) -> "Config": + self.__variables.update(variables) + return self + + def base_url(self, base_url: Text) -> "Config": + self.__base_url = base_url + return self + + def verify(self, verify: bool) -> "Config": + self.__verify = verify + return self + + def path(self, path: Text) -> "Config": + self.__path = path + return self + + def init(self) -> TConfig: + return TConfig( + name=self.__name, + base_url=self.__base_url, + verify=self.__verify, + variables=self.__variables, + path=self.__path, + ) + + +class RequestOptionalArgs(object): + def __init__(self, method: MethodEnum, url: Text): + self.__method = method + self.__url = url + self.__params = {} + self.__headers = {} + self.__cookies = {} + self.__data = "" + self.__timeout = 120 + self.__allow_redirects = True + self.__verify = False + + def with_params(self, **params) -> "RequestOptionalArgs": + self.__params.update(params) + return self + + def with_headers(self, **headers) -> "RequestOptionalArgs": + self.__headers.update(headers) + return self + + def with_cookies(self, **cookies) -> "RequestOptionalArgs": + self.__cookies.update(cookies) + return self + + def with_data(self, data) -> "RequestOptionalArgs": + self.__data = data + return self + + def set_timeout(self, timeout: float) -> "RequestOptionalArgs": + self.__timeout = timeout + return self + + def set_verify(self, verify: bool) -> "RequestOptionalArgs": + self.__verify = verify + return self + + def set_allow_redirects(self, allow_redirects: bool) -> "RequestOptionalArgs": + self.__allow_redirects = allow_redirects + return self + + def perform(self) -> TRequest: + """build TRequest object with configs""" + return TRequest( + method=self.__method, + url=self.__url, + params=self.__params, + headers=self.__headers, + data=self.__data, + timeout=self.__timeout, + verify=self.__verify, + allow_redirects=self.__allow_redirects, + ) + + +class Request(object): + def get(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.GET, url) + + def post(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.POST, url) + + def put(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.PUT, url) + + def head(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.HEAD, url) + + def delete(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.DELETE, url) + + def options(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.OPTIONS, url) + + def patch(self, url: Text) -> RequestOptionalArgs: + return RequestOptionalArgs(MethodEnum.PATCH, url) + + +class Step(object): + def __init__(self, name): + self.__name = name + self.__variables = {} + self.__request = None + self.__extract = {} + self.__validators = [] + + def with_variables(self, **variables) -> "Step": + self.__variables.update(variables) + return self + + def run_request(self, req_obj: RequestOptionalArgs) -> "Step": + self.__request = req_obj.perform() + return self + + def extract(self, var_name: Text, jmes_path: Text) -> "Step": + self.__extract[var_name] = jmes_path + return self + + def assert_equal(self, jmes_path: Text, expected_value: Any) -> "Step": + self.__validators.append({"eq": [jmes_path, expected_value]}) + return self + + def assert_greater_than(self, jmes_path: Text, expected_value: Any) -> "Step": + self.__validators.append({"gt": [jmes_path, expected_value]}) + return self + + def assert_less_than(self, jmes_path: Text, expected_value: Any) -> "Step": + self.__validators.append({"lt": [jmes_path, expected_value]}) + return self + + def init(self) -> TStep: + return TStep( + name=self.__name, + variables=self.__variables, + request=self.__request, + extract=self.__extract, + validate=self.__validators, + ) + From 2b30a1337b510ede0c5db78844129d3e7af322c8 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 14:27:52 +0800 Subject: [PATCH 12/32] refactor: HttpRunner config & teststeps --- .../postman_echo/request_methods/conftest.py | 7 +- .../request_with_functions_test.py | 12 ++-- httprunner/runner.py | 51 ++++++++------ httprunner/testcase.py | 68 +++++++++++-------- 4 files changed, 78 insertions(+), 60 deletions(-) diff --git a/examples/postman_echo/request_methods/conftest.py b/examples/postman_echo/request_methods/conftest.py index 4f0e444c..788c2686 100644 --- a/examples/postman_echo/request_methods/conftest.py +++ b/examples/postman_echo/request_methods/conftest.py @@ -2,10 +2,9 @@ import uuid from typing import List import pytest +from httprunner import Config, Step from loguru import logger -from httprunner.schema import TConfig, TStep - @pytest.fixture(scope="session", autouse=True) def session_fixture(request): @@ -33,8 +32,8 @@ def session_fixture(request): @pytest.fixture(scope="function", autouse=True) def testcase_fixture(request): """setup and teardown each testcase""" - config: TConfig = request.cls.config - teststeps: List[TStep] = request.cls.teststeps + config: Config = request.cls.config + teststeps: List[Step] = request.cls.teststeps logger.debug(f"setup testcase fixture: {config.name} - {request.module.__name__}") 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 c8fad6a6..49031e9a 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -10,10 +10,9 @@ class TestCaseRequestWithFunctions(HttpRunner): .variables(foo1="session_bar1") .base_url("https://postman-echo.com") .verify(False) - .path( + .set_path( "examples/postman_echo/request_methods/request_with_functions_test.py" ) - .init() ) teststeps = [ @@ -29,8 +28,7 @@ class TestCaseRequestWithFunctions(HttpRunner): .assert_equal("status_code", 200) .assert_equal("body.args.foo1", "session_bar1") .assert_equal("body.args.sum_v", "3") - .assert_equal("body.args.foo2", "session_bar2") - .init(), + .assert_equal("body.args.foo2", "session_bar2"), Step("post raw text") .with_variables(foo1="hello world", foo3="$session_foo2") .run_request( @@ -50,8 +48,7 @@ class TestCaseRequestWithFunctions(HttpRunner): .assert_equal( "body.data", "This is expected to be sent back as part of response body: session_bar1-session_bar2.", - ) - .init(), + ), Step("post form data") .with_variables(**{"foo1": "bar1", "foo2": "bar2"}) .run_request( @@ -67,8 +64,7 @@ class TestCaseRequestWithFunctions(HttpRunner): ) .assert_equal("status_code", 200) .assert_equal("body.form.foo1", "session_bar1") - .assert_equal("body.form.foo2", "bar2") - .init(), + .assert_equal("body.form.foo2", "bar2"), ] diff --git a/httprunner/runner.py b/httprunner/runner.py index 20663408..c4101321 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -20,6 +20,7 @@ from httprunner.ext.uploader import prepare_upload_step from httprunner.loader import load_project_meta, load_testcase_file from httprunner.parser import build_url, parse_data, parse_variables_mapping from httprunner.response import ResponseObject +from httprunner.testcase import Config, Step from httprunner.schema import ( TConfig, TStep, @@ -34,10 +35,12 @@ from httprunner.schema import ( class HttpRunner(object): - config: TConfig - teststeps: List[TStep] + config: Config + teststeps: List[Step] success: bool = True # indicate testcase execution result + __config: TConfig + __teststeps: List[TStep] __project_meta: ProjectMeta = None __case_id: Text = "" __step_datas: List[StepData] = None @@ -84,7 +87,7 @@ class HttpRunner(object): # prepare arguments method = parsed_request_dict.pop("method") url_path = parsed_request_dict.pop("url") - url = build_url(self.config.base_url, url_path) + url = build_url(self.__config.base_url, url_path) parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {}) # request @@ -220,21 +223,23 @@ class HttpRunner(object): >>> HttpRunner().with_project_meta(project_meta).run_testcase(testcase_obj) """ - self.config = testcase.config - self.teststeps = testcase.teststeps + self.__config = testcase.config + self.__teststeps = testcase.teststeps # prepare - self.__project_meta = self.__project_meta or load_project_meta(self.config.path) - self.__parse_config(self.config) + self.__project_meta = self.__project_meta or load_project_meta( + self.__config.path + ) + self.__parse_config(self.__config) self.__start_at = time.time() self.__step_datas: List[StepData] = [] self.__session = self.__session or HttpSession() self.__session_variables = {} # run teststeps - for step in self.teststeps: + for step in self.__teststeps: # update with config variables - step.variables.update(self.config.variables) + step.variables.update(self.__config.variables) # update with session variables extracted from pre step step.variables.update(self.__session_variables) # parse variables @@ -267,7 +272,9 @@ class HttpRunner(object): >>> TestCaseRequestWithFunctions().run() """ - testcase_obj = TestCase(config=self.config, teststeps=self.teststeps) + self.__config = self.config.perform() + self.__teststeps = [step.perform() for step in self.teststeps] + testcase_obj = TestCase(config=self.__config, teststeps=self.__teststeps) return self.run_testcase(testcase_obj) def get_step_datas(self) -> List[StepData]: @@ -275,7 +282,7 @@ class HttpRunner(object): def get_export_variables(self) -> Dict: export_vars_mapping = {} - for var_name in self.config.export: + for var_name in self.__config.export: if var_name not in self.__session_variables: raise ParamsError( f"failed to export variable {var_name} from session variables {self.__session_variables}" @@ -290,7 +297,7 @@ class HttpRunner(object): start_at_timestamp = self.__start_at start_at_iso_format = datetime.utcfromtimestamp(start_at_timestamp).isoformat() return TestCaseSummary( - name=self.config.name, + name=self.__config.name, success=self.success, case_id=self.__case_id, time=TestCaseTime( @@ -299,7 +306,7 @@ class HttpRunner(object): duration=self.__duration, ), in_out=TestCaseInOut( - vars=self.config.variables, export=self.get_export_variables() + vars=self.__config.variables, export=self.get_export_variables() ), log=self.__log_path, step_datas=self.__step_datas, @@ -307,7 +314,11 @@ class HttpRunner(object): def test_start(self) -> "HttpRunner": """main entrance, discovered by pytest""" - self.__project_meta = self.__project_meta or load_project_meta(self.config.path) + self.__config = self.config.perform() + self.__teststeps = [step.perform() for step in self.teststeps] + self.__project_meta = self.__project_meta or load_project_meta( + self.__config.path + ) self.__case_id = self.__case_id or str(uuid.uuid4()) self.__log_path = self.__log_path or os.path.join( self.__project_meta.PWD, "logs", f"{self.__case_id}.run.log" @@ -315,24 +326,24 @@ class HttpRunner(object): log_handler = logger.add(self.__log_path, level="DEBUG") # parse config name - variables = self.config.variables + variables = self.__config.variables variables.update(self.__session_variables) - self.config.name = parse_data( - self.config.name, variables, self.__project_meta.functions + self.__config.name = parse_data( + self.__config.name, variables, self.__project_meta.functions ) if USE_ALLURE: # update allure report meta - allure.dynamic.title(self.config.name) + allure.dynamic.title(self.__config.name) allure.dynamic.description(f"TestCase ID: {self.__case_id}") logger.info( - f"Start to run testcase: {self.config.name}, TestCase ID: {self.__case_id}" + f"Start to run testcase: {self.__config.name}, TestCase ID: {self.__case_id}" ) try: return self.run_testcase( - TestCase(config=self.config, teststeps=self.teststeps) + TestCase(config=self.__config, teststeps=self.__teststeps) ) finally: logger.remove(log_handler) diff --git a/httprunner/testcase.py b/httprunner/testcase.py index dafd4f67..0ed30d4d 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -1,4 +1,4 @@ -from typing import Text, Any +from typing import Text, Any, Dict from httprunner.schema import ( TConfig, @@ -16,6 +16,14 @@ class Config(object): self.__verify = False self.__path = "" + @property + def name(self): + return self.__name + + @property + def path(self): + return self.__path + def variables(self, **variables) -> "Config": self.__variables.update(variables) return self @@ -28,11 +36,11 @@ class Config(object): self.__verify = verify return self - def path(self, path: Text) -> "Config": + def set_path(self, path: Text) -> "Config": self.__path = path return self - def init(self) -> TConfig: + def perform(self) -> TConfig: return TConfig( name=self.__name, base_url=self.__base_url, @@ -42,7 +50,7 @@ class Config(object): ) -class RequestOptionalArgs(object): +class RequestWithOptionalArgs(object): def __init__(self, method: MethodEnum, url: Text): self.__method = method self.__url = url @@ -54,31 +62,31 @@ class RequestOptionalArgs(object): self.__allow_redirects = True self.__verify = False - def with_params(self, **params) -> "RequestOptionalArgs": + def with_params(self, **params) -> "RequestWithOptionalArgs": self.__params.update(params) return self - def with_headers(self, **headers) -> "RequestOptionalArgs": + def with_headers(self, **headers) -> "RequestWithOptionalArgs": self.__headers.update(headers) return self - def with_cookies(self, **cookies) -> "RequestOptionalArgs": + def with_cookies(self, **cookies) -> "RequestWithOptionalArgs": self.__cookies.update(cookies) return self - def with_data(self, data) -> "RequestOptionalArgs": + def with_data(self, data) -> "RequestWithOptionalArgs": self.__data = data return self - def set_timeout(self, timeout: float) -> "RequestOptionalArgs": + def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs": self.__timeout = timeout return self - def set_verify(self, verify: bool) -> "RequestOptionalArgs": + def set_verify(self, verify: bool) -> "RequestWithOptionalArgs": self.__verify = verify return self - def set_allow_redirects(self, allow_redirects: bool) -> "RequestOptionalArgs": + def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs": self.__allow_redirects = allow_redirects return self @@ -97,30 +105,30 @@ class RequestOptionalArgs(object): class Request(object): - def get(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.GET, url) + def get(self, url: Text) -> RequestWithOptionalArgs: + return RequestWithOptionalArgs(MethodEnum.GET, url) - def post(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.POST, url) + def post(self, url: Text) -> RequestWithOptionalArgs: + return RequestWithOptionalArgs(MethodEnum.POST, url) - def put(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.PUT, url) + def put(self, url: Text) -> RequestWithOptionalArgs: + return RequestWithOptionalArgs(MethodEnum.PUT, url) - def head(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.HEAD, url) + def head(self, url: Text) -> RequestWithOptionalArgs: + return RequestWithOptionalArgs(MethodEnum.HEAD, url) - def delete(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.DELETE, url) + def delete(self, url: Text) -> RequestWithOptionalArgs: + return RequestWithOptionalArgs(MethodEnum.DELETE, url) - def options(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.OPTIONS, url) + def options(self, url: Text) -> RequestWithOptionalArgs: + return RequestWithOptionalArgs(MethodEnum.OPTIONS, url) - def patch(self, url: Text) -> RequestOptionalArgs: - return RequestOptionalArgs(MethodEnum.PATCH, url) + def patch(self, url: Text) -> RequestWithOptionalArgs: + return RequestWithOptionalArgs(MethodEnum.PATCH, url) class Step(object): - def __init__(self, name): + def __init__(self, name: Text): self.__name = name self.__variables = {} self.__request = None @@ -131,7 +139,11 @@ class Step(object): self.__variables.update(variables) return self - def run_request(self, req_obj: RequestOptionalArgs) -> "Step": + @property + def request(self) -> TRequest: + return self.__request + + def run_request(self, req_obj: RequestWithOptionalArgs) -> "Step": self.__request = req_obj.perform() return self @@ -151,7 +163,7 @@ class Step(object): self.__validators.append({"lt": [jmes_path, expected_value]}) return self - def init(self) -> TStep: + def perform(self) -> TStep: return TStep( name=self.__name, variables=self.__variables, From a4659066261376cb7bcea9d3dd451c672696f7eb Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 16:55:36 +0800 Subject: [PATCH 13/32] refactor: get config path with inspect stack --- .../request_methods/request_with_functions_test.py | 3 --- httprunner/testcase.py | 12 +++++------- 2 files changed, 5 insertions(+), 10 deletions(-) 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 49031e9a..520608c1 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -10,9 +10,6 @@ class TestCaseRequestWithFunctions(HttpRunner): .variables(foo1="session_bar1") .base_url("https://postman-echo.com") .verify(False) - .set_path( - "examples/postman_echo/request_methods/request_with_functions_test.py" - ) ) teststeps = [ diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 0ed30d4d..38334879 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -1,4 +1,5 @@ -from typing import Text, Any, Dict +import inspect +from typing import Text, Any from httprunner.schema import ( TConfig, @@ -14,7 +15,9 @@ class Config(object): self.__variables = {} self.__base_url = "" self.__verify = False - self.__path = "" + + caller_frame = inspect.stack()[1] + self.__path = caller_frame.filename @property def name(self): @@ -36,10 +39,6 @@ class Config(object): self.__verify = verify return self - def set_path(self, path: Text) -> "Config": - self.__path = path - return self - def perform(self) -> TConfig: return TConfig( name=self.__name, @@ -171,4 +170,3 @@ class Step(object): extract=self.__extract, validate=self.__validators, ) - From ca28d1bdb3233e27ca2558829d7504b12757c400 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 17:46:21 +0800 Subject: [PATCH 14/32] refactor: StepValidation --- .../request_with_functions_test.py | 2 +- httprunner/testcase.py | 60 ++++++++++++------- 2 files changed, 39 insertions(+), 23 deletions(-) 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 520608c1..e084d808 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -15,13 +15,13 @@ class TestCaseRequestWithFunctions(HttpRunner): teststeps = [ Step("get with params") .with_variables(foo1="bar1", foo2="session_bar2", sum_v="${sum_two(1, 2)}") + .set_extractor("session_foo2", "body.args.foo2") .run_request( Request() .get("/get") .with_params(foo1="$foo1", foo2="$foo2", sum_v="$sum_v") .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) ) - .extract("session_foo2", "body.args.foo2") .assert_equal("status_code", 200) .assert_equal("body.args.foo1", "session_bar1") .assert_equal("body.args.sum_v", "3") diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 38334879..9e94d3cf 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -1,5 +1,5 @@ import inspect -from typing import Text, Any +from typing import Text, Any, Dict from httprunner.schema import ( TConfig, @@ -126,39 +126,33 @@ class Request(object): return RequestWithOptionalArgs(MethodEnum.PATCH, url) -class Step(object): - def __init__(self, name: Text): +class StepValidation(object): + def __init__( + self, name: Text, variables: Dict, extractors: Dict, request: TRequest + ): self.__name = name - self.__variables = {} - self.__request = None - self.__extract = {} + self.__variables = variables + self.__extractors = extractors + self.__request = request self.__validators = [] - def with_variables(self, **variables) -> "Step": - self.__variables.update(variables) - return self - @property def request(self) -> TRequest: return self.__request - def run_request(self, req_obj: RequestWithOptionalArgs) -> "Step": - self.__request = req_obj.perform() - return self - - def extract(self, var_name: Text, jmes_path: Text) -> "Step": - self.__extract[var_name] = jmes_path - return self - - def assert_equal(self, jmes_path: Text, expected_value: Any) -> "Step": + def assert_equal(self, jmes_path: Text, expected_value: Any) -> "StepValidation": self.__validators.append({"eq": [jmes_path, expected_value]}) return self - def assert_greater_than(self, jmes_path: Text, expected_value: Any) -> "Step": + def assert_greater_than( + self, jmes_path: Text, expected_value: Any + ) -> "StepValidation": self.__validators.append({"gt": [jmes_path, expected_value]}) return self - def assert_less_than(self, jmes_path: Text, expected_value: Any) -> "Step": + def assert_less_than( + self, jmes_path: Text, expected_value: Any + ) -> "StepValidation": self.__validators.append({"lt": [jmes_path, expected_value]}) return self @@ -167,6 +161,28 @@ class Step(object): name=self.__name, variables=self.__variables, request=self.__request, - extract=self.__extract, + extract=self.__extractors, validate=self.__validators, ) + + +class Step(object): + def __init__(self, name: Text): + self.__name = name + self.__variables = {} + self.__extractors = {} + self.__request = None + + def with_variables(self, **variables) -> "Step": + self.__variables.update(variables) + return self + + def set_extractor(self, var_name: Text, jmes_path: Text) -> "Step": + self.__extractors[var_name] = jmes_path + return self + + def run_request(self, req_obj: RequestWithOptionalArgs) -> "StepValidation": + self.__request = req_obj.perform() + return StepValidation( + self.__name, self.__variables, self.__extractors, self.__request + ) From 5b0bc407866c324c8d6ad537f81a6ba9e7108cac Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 18:26:41 +0800 Subject: [PATCH 15/32] fix: compatibility --- httprunner/runner.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/httprunner/runner.py b/httprunner/runner.py index c4101321..9a1c4774 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -2,7 +2,7 @@ import os import time import uuid from datetime import datetime -from typing import List, Dict, Text, NoReturn +from typing import List, Dict, Text, NoReturn, Union try: import allure @@ -20,7 +20,7 @@ from httprunner.ext.uploader import prepare_upload_step from httprunner.loader import load_project_meta, load_testcase_file from httprunner.parser import build_url, parse_data, parse_variables_mapping from httprunner.response import ResponseObject -from httprunner.testcase import Config, Step +from httprunner.testcase import Config, StepValidation from httprunner.schema import ( TConfig, TStep, @@ -35,8 +35,8 @@ from httprunner.schema import ( class HttpRunner(object): - config: Config - teststeps: List[Step] + config: Union[TConfig, Config] + teststeps: List[Union[TStep, StepValidation]] success: bool = True # indicate testcase execution result __config: TConfig @@ -52,6 +52,23 @@ class HttpRunner(object): # log __log_path: Text = "" + def __init_tests__(self) -> NoReturn: + if isinstance(self.config, TConfig): + self.__config = self.config + elif isinstance(self.config, Config): + self.__config = self.config.perform() + else: + raise exceptions.TestCaseFormatError(f"config type error: {self.config}") + + self.__teststeps = [] + for step in self.teststeps: + if isinstance(step, TStep): + self.__teststeps.append(step) + elif isinstance(step, StepValidation): + self.__teststeps.append(step.perform()) + else: + raise exceptions.TestCaseFormatError(f"step type error: {step}") + def with_project_meta(self, project_meta: ProjectMeta) -> "HttpRunner": self.__project_meta = project_meta return self @@ -272,8 +289,7 @@ class HttpRunner(object): >>> TestCaseRequestWithFunctions().run() """ - self.__config = self.config.perform() - self.__teststeps = [step.perform() for step in self.teststeps] + self.__init_tests__() testcase_obj = TestCase(config=self.__config, teststeps=self.__teststeps) return self.run_testcase(testcase_obj) @@ -314,8 +330,7 @@ class HttpRunner(object): def test_start(self) -> "HttpRunner": """main entrance, discovered by pytest""" - self.__config = self.config.perform() - self.__teststeps = [step.perform() for step in self.teststeps] + self.__init_tests__() self.__project_meta = self.__project_meta or load_project_meta( self.__config.path ) From 1b7f9b334bb6ad65fd2c9193783ffde7aeba3d9c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 1 Jun 2020 18:56:54 +0800 Subject: [PATCH 16/32] feat: describe testcase in chain-call style, run testcase --- .../request_with_testcase_reference_test.py | 25 +++++++---------- httprunner/schema.py | 4 +-- httprunner/testcase.py | 27 ++++++++++++++----- 3 files changed, 32 insertions(+), 24 deletions(-) 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 7f87b2d6..cb2fc35a 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 @@ -6,7 +6,7 @@ import sys sys.path.insert(0, os.getcwd()) -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step from examples.postman_echo.request_methods.request_with_functions_test import ( TestCaseRequestWithFunctions as RequestWithFunctions, @@ -14,24 +14,17 @@ from examples.postman_echo.request_methods.request_with_functions_test import ( class TestCaseRequestWithTestcaseReference(HttpRunner): - config = TConfig( - **{ - "name": "request methods testcase: reference testcase", - "variables": {"foo1": "session_bar1"}, - "base_url": "https://postman-echo.com", - "verify": False, - "path": "examples/postman_echo/request_methods/request_with_testcase_reference_test.py", - } + config = ( + Config("request methods testcase: reference testcase") + .variables(foo1="session_bar1") + .base_url("https://postman-echo.com") + .verify(False) ) teststeps = [ - TStep( - **{ - "name": "request with functions", - "variables": {"foo1": "override_bar1"}, - "testcase": RequestWithFunctions, - } - ), + Step("request with functions") + .with_variables(foo1="override_bar1") + .run_testcase(RequestWithFunctions), ] diff --git a/httprunner/schema.py b/httprunner/schema.py index 7599b8c6..23faf395 100644 --- a/httprunner/schema.py +++ b/httprunner/schema.py @@ -61,8 +61,8 @@ class TRequest(BaseModel): class TStep(BaseModel): name: Name - request: TRequest = None - testcase: Union[Text, Callable] = "" + request: Union[TRequest, None] = None + testcase: Union[Text, Callable, None] = None variables: VariablesMapping = {} setup_hooks: Hook = [] teardown_hooks: Hook = [] diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 9e94d3cf..a2af4c4a 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -1,11 +1,12 @@ import inspect -from typing import Text, Any, Dict +from typing import Text, Any, Dict, Callable from httprunner.schema import ( TConfig, TStep, TRequest, MethodEnum, + TestCase, ) @@ -128,18 +129,28 @@ class Request(object): class StepValidation(object): def __init__( - self, name: Text, variables: Dict, extractors: Dict, request: TRequest + self, + name: Text, + variables: Dict, + extractors: Dict, + request: TRequest = None, + testcase: Callable = None, ): self.__name = name self.__variables = variables self.__extractors = extractors - self.__request = request + self.__request: TRequest = request + self.__testcase: Callable = testcase self.__validators = [] @property def request(self) -> TRequest: return self.__request + @property + def testcase(self) -> TestCase: + return self.__testcase + def assert_equal(self, jmes_path: Text, expected_value: Any) -> "StepValidation": self.__validators.append({"eq": [jmes_path, expected_value]}) return self @@ -161,6 +172,7 @@ class StepValidation(object): name=self.__name, variables=self.__variables, request=self.__request, + testcase=self.__testcase, extract=self.__extractors, validate=self.__validators, ) @@ -171,7 +183,6 @@ class Step(object): self.__name = name self.__variables = {} self.__extractors = {} - self.__request = None def with_variables(self, **variables) -> "Step": self.__variables.update(variables) @@ -182,7 +193,11 @@ class Step(object): return self def run_request(self, req_obj: RequestWithOptionalArgs) -> "StepValidation": - self.__request = req_obj.perform() return StepValidation( - self.__name, self.__variables, self.__extractors, self.__request + self.__name, self.__variables, self.__extractors, request=req_obj.perform() + ) + + def run_testcase(self, testcase: Callable) -> "StepValidation": + return StepValidation( + self.__name, self.__variables, self.__extractors, testcase=testcase ) From f2034426d54e2e9e6f68524de545673ab2f6898d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 2 Jun 2020 14:13:14 +0800 Subject: [PATCH 17/32] change: builtin comparator equal --- httprunner/builtin/comparators.py | 2 +- httprunner/response.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/httprunner/builtin/comparators.py b/httprunner/builtin/comparators.py index 975c868d..8a0f1485 100644 --- a/httprunner/builtin/comparators.py +++ b/httprunner/builtin/comparators.py @@ -5,7 +5,7 @@ Built-in validate comparators. import re -def equals(check_value, expect_value): +def equal(check_value, expect_value): assert check_value == expect_value diff --git a/httprunner/response.py b/httprunner/response.py index 806d7f86..7733b90c 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -12,8 +12,8 @@ from httprunner.schema import VariablesMapping, Validators, FunctionsMapping def get_uniform_comparator(comparator: Text): """ convert comparator alias to uniform name """ - if comparator in ["eq", "equals", "==", "is"]: - return "equals" + if comparator in ["eq", "equals", "equal"]: + return "equal" elif comparator in ["lt", "less_than"]: return "less_than" elif comparator in ["le", "less_than_or_equals"]: @@ -62,8 +62,8 @@ def uniform_validator(validator): validator (dict): validator maybe in two formats: format1: this is kept for compatibility with the previous versions. - {"check": "status_code", "assert": "eq", "expect": 201} - {"check": "$resp_body_success", "assert": "eq", "expect": True} + {"check": "status_code", "comparator": "eq", "expect": 201} + {"check": "$resp_body_success", "comparator": "eq", "expect": True} format2: recommended new version, {assert: [check_item, expected_value]} {'eq': ['status_code', 201]} {'eq': ['$resp_body_success', True]} From 6b9bd46079ad05a0438a3ba369371818b4f2f49e Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 2 Jun 2020 14:37:37 +0800 Subject: [PATCH 18/32] feat: make pytest files in chain style --- examples/postman_echo/conftest.py | 62 ++++ .../cookie_manipulation/__init__.py | 0 .../cookie_manipulation/hardcode.yml | 34 +++ .../cookie_manipulation/hardcode_test.py | 54 ++++ .../cookie_manipulation/set_delete_cookies.py | 62 ++++ .../set_delete_cookies_test.py | 59 ++++ .../demo_testsuite_yml/__init__.py | 2 +- .../request_with_functions_test.py | 128 ++++----- .../request_with_testcase_reference_test.py | 25 +- .../request_methods/hardcode_test.py | 104 +++---- .../request_with_functions_test.py | 129 +++++---- .../request_with_testcase_reference_test.py | 12 +- .../request_with_variables_test.py | 117 ++++---- .../validate_with_functions_test.py | 48 ++-- .../validate_with_variables_test.py | 117 ++++---- httprunner/__init__.py | 5 +- httprunner/cli.py | 9 +- httprunner/make.py | 171 ++++++++++- httprunner/runner.py | 6 +- httprunner/testcase.py | 272 +++++++++--------- tests/make_test.py | 47 ++- 21 files changed, 937 insertions(+), 526 deletions(-) create mode 100644 examples/postman_echo/conftest.py create mode 100644 examples/postman_echo/cookie_manipulation/__init__.py create mode 100644 examples/postman_echo/cookie_manipulation/hardcode.yml create mode 100644 examples/postman_echo/cookie_manipulation/hardcode_test.py create mode 100644 examples/postman_echo/cookie_manipulation/set_delete_cookies.py create mode 100644 examples/postman_echo/cookie_manipulation/set_delete_cookies_test.py diff --git a/examples/postman_echo/conftest.py b/examples/postman_echo/conftest.py new file mode 100644 index 00000000..9bb834e6 --- /dev/null +++ b/examples/postman_echo/conftest.py @@ -0,0 +1,62 @@ +# NOTICE: Generated By HttpRunner. +import json +import os +import time + +import pytest +from loguru import logger + +from httprunner.utils import get_platform + + +@pytest.fixture(scope="session", autouse=True) +def session_fixture(request): + """setup and teardown each task""" + logger.info(f"start running testcases ...") + + start_at = time.time() + + yield + + logger.info(f"task finished, generate task summary for --save-tests") + + summary = { + "success": True, + "stat": { + "testcases": {"total": 0, "success": 0, "fail": 0}, + "teststeps": {"total": 0, "failures": 0, "successes": 0}, + }, + "time": {"start_at": start_at, "duration": time.time() - start_at}, + "platform": get_platform(), + "details": [], + } + + for item in request.node.items: + testcase_summary = item.instance.get_summary() + summary["success"] &= testcase_summary.success + + summary["stat"]["testcases"]["total"] += 1 + summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_datas) + if testcase_summary.success: + summary["stat"]["testcases"]["success"] += 1 + summary["stat"]["teststeps"]["successes"] += len( + testcase_summary.step_datas + ) + else: + summary["stat"]["testcases"]["fail"] += 1 + summary["stat"]["teststeps"]["successes"] += ( + len(testcase_summary.step_datas) - 1 + ) + summary["stat"]["teststeps"]["failures"] += 1 + + summary["details"].append(testcase_summary.dict()) + + summary_path = "/Users/debugtalk/MyProjects/HttpRunner-dev/HttpRunner/examples/postman_echo/logs/request_methods/hardcode.summary.json" + summary_dir = os.path.dirname(summary_path) + os.makedirs(summary_dir, exist_ok=True) + + with open(summary_path, "w", encoding="utf-8") as f: + json.dump(summary, f, indent=4) + + logger.info(f"generated task summary: {summary_path}") + diff --git a/examples/postman_echo/cookie_manipulation/__init__.py b/examples/postman_echo/cookie_manipulation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/postman_echo/cookie_manipulation/hardcode.yml b/examples/postman_echo/cookie_manipulation/hardcode.yml new file mode 100644 index 00000000..f81c5f7c --- /dev/null +++ b/examples/postman_echo/cookie_manipulation/hardcode.yml @@ -0,0 +1,34 @@ +config: + name: "set & delete cookies." + base_url: "https://postman-echo.com" + verify: False + export: ["cookie_foo1", "cookie_foo3"] + +teststeps: +- + name: set cookie foo1 & foo2 & foo3 + request: + method: GET + url: /cookies/set + params: + foo1: bar1 + foo2: bar2 + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + extract: + cookie_foo1: $.cookies.foo1 + validate: + - eq: ["status_code", 200] + - eq: ["cookies.foo1", "bar1"] +- + name: delete cookie foo2 + request: + method: GET + url: /cookies/delete?foo2 + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + validate: + - eq: ["status_code", 200] + - ne: ["$.cookies.foo1", "$foo1"] + - eq: ["$.cookies.foo1", "$cookie_foo1"] + - eq: ["$.cookies.foo3", "$cookie_foo3"] diff --git a/examples/postman_echo/cookie_manipulation/hardcode_test.py b/examples/postman_echo/cookie_manipulation/hardcode_test.py new file mode 100644 index 00000000..000d0404 --- /dev/null +++ b/examples/postman_echo/cookie_manipulation/hardcode_test.py @@ -0,0 +1,54 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# FROM: examples/postman_echo/cookie_manipulation/hardcode.yml +from httprunner import HttpRunner, TConfig, TStep + + +class TestCaseHardcode(HttpRunner): + config = TConfig( + **{ + "name": "set & delete cookies.", + "base_url": "https://postman-echo.com", + "verify": False, + "export": ["cookie_foo1", "cookie_foo3"], + "path": "examples/postman_echo/cookie_manipulation/hardcode_test.py", + } + ) + + teststeps = [ + TStep( + **{ + "name": "set cookie foo1 & foo2 & foo3", + "request": { + "method": "GET", + "url": "/cookies/set", + "params": {"foo1": "bar1", "foo2": "bar2"}, + "headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"}, + }, + "extract": {"cookie_foo1": "$.cookies.foo1"}, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["cookies.foo1", "bar1"]}, + ], + } + ), + TStep( + **{ + "name": "delete cookie foo2", + "request": { + "method": "GET", + "url": "/cookies/delete?foo2", + "headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"}, + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"ne": ["$.cookies.foo1", "$foo1"]}, + {"eq": ["$.cookies.foo1", "$cookie_foo1"]}, + {"eq": ["$.cookies.foo3", "$cookie_foo3"]}, + ], + } + ), + ] + + +if __name__ == "__main__": + TestCaseHardcode().test_start() diff --git a/examples/postman_echo/cookie_manipulation/set_delete_cookies.py b/examples/postman_echo/cookie_manipulation/set_delete_cookies.py new file mode 100644 index 00000000..b2e3bf26 --- /dev/null +++ b/examples/postman_echo/cookie_manipulation/set_delete_cookies.py @@ -0,0 +1,62 @@ +import unittest +import requests + +from httprunner.runner import HttpRunner +from httprunner.schema import TConfig, TStep + + +class TestCaseSetDeleteCookies(unittest.TestCase): + config = TConfig( + **{ + "name": "set & delete cookies.", + "base_url": "https://postman-echo.com", + "variables": {"foo1": "bar1", "foo2": "bar2"}, + "verify": False, + "export": ["cookie_foo1", "cookie_foo3"], + } + ) + + teststeps = [ + TStep( + **{ + "name": "set cookie foo1 & foo2 & foo3", + "variables": {"foo3": "bar3"}, + "request": { + "method": "GET", + "url": "/cookies/set", + "params": {"foo1": "bar111", "foo2": "$foo2", "foo3": "$foo3"}, + "headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"}, + }, + "extract": { + "cookie_foo1": "$.cookies.foo1", + "cookie_foo3": "$.cookies.foo3", + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["$.cookies.foo3", "$foo3"]}, + ], + } + ), + TStep( + **{ + "name": "delete cookie foo2", + "request": { + "method": "GET", + "url": "/cookies/delete?foo2", + "headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"}, + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"ne": ["$.cookies.foo1", "$foo1"]}, + {"eq": ["$.cookies.foo1", "$cookie_foo1"]}, + {"eq": ["$.cookies.foo3", "$cookie_foo3"]}, + ], + } + ), + ] + + def test_start(self): + s = requests.Session() + HttpRunner(self.config, self.teststeps, session=s).with_variables( + foo1="bar123", foo2="bar22" + ).run() diff --git a/examples/postman_echo/cookie_manipulation/set_delete_cookies_test.py b/examples/postman_echo/cookie_manipulation/set_delete_cookies_test.py new file mode 100644 index 00000000..540a2eb1 --- /dev/null +++ b/examples/postman_echo/cookie_manipulation/set_delete_cookies_test.py @@ -0,0 +1,59 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# FROM: examples/postman_echo/cookie_manipulation/set_delete_cookies.yml +from httprunner import HttpRunner, TConfig, TStep + + +class TestCaseSetDeleteCookies(HttpRunner): + config = TConfig( + **{ + "name": "set & delete cookies.", + "variables": {"foo1": "bar1", "foo2": "bar2"}, + "base_url": "https://postman-echo.com", + "verify": False, + "export": ["cookie_foo1", "cookie_foo3"], + "path": "examples/postman_echo/cookie_manipulation/set_delete_cookies_test.py", + } + ) + + teststeps = [ + TStep( + **{ + "name": "set cookie foo1 & foo2 & foo3", + "variables": {"foo3": "bar3"}, + "request": { + "method": "GET", + "url": "/cookies/set", + "params": {"foo1": "bar111", "foo2": "$foo2", "foo3": "$foo3"}, + "headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"}, + }, + "extract": { + "cookie_foo1": "$.cookies.foo1", + "cookie_foo3": "$.cookies.foo3", + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"ne": ["$.cookies.foo3", "$foo3"]}, + ], + } + ), + TStep( + **{ + "name": "delete cookie foo2", + "request": { + "method": "GET", + "url": "/cookies/delete?foo2", + "headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"}, + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"ne": ["$.cookies.foo1", "$foo1"]}, + {"eq": ["$.cookies.foo1", "$cookie_foo1"]}, + {"eq": ["$.cookies.foo3", "$cookie_foo3"]}, + ], + } + ), + ] + + +if __name__ == "__main__": + TestCaseSetDeleteCookies().test_start() diff --git a/examples/postman_echo/request_methods/demo_testsuite_yml/__init__.py b/examples/postman_echo/request_methods/demo_testsuite_yml/__init__.py index bed305d5..70cfba53 100644 --- a/examples/postman_echo/request_methods/demo_testsuite_yml/__init__.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/__init__.py @@ -1 +1 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! \ No newline at end of file +# NOTICE: Generated By HttpRunner. DO NOT EDIT! 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 0bc92c4d..01d3e075 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,87 +1,69 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/postman_echo/request_methods/request_with_functions.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase 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_yml/request_with_functions_test.py", - } + config = ( + Config("request with functions") + .variables(**{"foo1": "session_bar1", "var1": "testsuite_val1"}) + .base_url("https://postman-echo.com") + .verify(False) ) 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"]}, - ], - } + Step( + RunRequest("get with params") + .with_variables( + **{"foo1": "bar1", "foo2": "session_bar2", "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.foo2", "session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.foo1", "session_bar1") + .assert_equal("body.args.sum_v", "3") + .assert_equal("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.", - ] - }, - ], - } + Step( + RunRequest("post raw text") + .with_variables(**{"foo1": "hello world", "foo3": "$session_foo2"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "text/plain", + } + ) + .with_data( + "This is expected to be sent back as part of response body: $foo1-$foo3." + ) + .validate() + .assert_equal("status_code", 200) + .assert_equal( + "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"]}, - ], - } + Step( + RunRequest("post form data") + .with_variables(**{"foo1": "bar1", "foo2": "bar2"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "session_bar1") + .assert_equal("body.form.foo2", "bar2") ), ] 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 187b5e23..dac68fef 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 @@ -6,7 +6,7 @@ import sys sys.path.insert(0, os.getcwd()) -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from examples.postman_echo.request_methods.request_with_functions_test import ( TestCaseRequestWithFunctions as RequestWithFunctions, @@ -14,23 +14,18 @@ from examples.postman_echo.request_methods.request_with_functions_test import ( 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_yml/request_with_testcase_reference_test.py", - } + config = ( + Config("request with referenced testcase") + .variables(**{"foo1": "session_bar1", "var2": "testsuite_val2"}) + .base_url("https://postman-echo.com") + .verify(False) ) teststeps = [ - TStep( - **{ - "name": "request with functions", - "variables": {"foo1": "override_bar1"}, - "testcase": RequestWithFunctions, - } + Step( + RunTestCase("request with functions") + .with_variables(**{"foo1": "override_bar1"}) + .call(RequestWithFunctions) ), ] diff --git a/examples/postman_echo/request_methods/hardcode_test.py b/examples/postman_echo/request_methods/hardcode_test.py index f9931709..4fd3aa2c 100644 --- a/examples/postman_echo/request_methods/hardcode_test.py +++ b/examples/postman_echo/request_methods/hardcode_test.py @@ -1,77 +1,57 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/postman_echo/request_methods/hardcode.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseHardcode(HttpRunner): - config = TConfig( - **{ - "name": "request methods testcase in hardcode", - "base_url": "https://postman-echo.com", - "verify": False, - "path": "examples/postman_echo/request_methods/hardcode_test.py", - "variables": {}, - } + config = ( + Config("request methods testcase in hardcode") + .base_url("https://postman-echo.com") + .verify(False) ) teststeps = [ - TStep( - **{ - "name": "get with params", - "request": { - "method": "GET", - "url": "/get", - "params": {"foo1": "bar1", "foo2": "bar2"}, - "headers": {"User-Agent": "HttpRunner/3.0"}, - }, - "validate": [{"eq": ["status_code", 200]}], - } + Step( + RunRequest("get with params") + .get("/get") + .with_params(**{"foo1": "bar1", "foo2": "bar2"}) + .with_headers(**{"User-Agent": "HttpRunner/3.0"}) + .validate() + .assert_equal("status_code", 200) ), - TStep( - **{ - "name": "post raw text", - "request": { - "method": "POST", - "url": "/post", - "headers": { - "User-Agent": "HttpRunner/3.0", - "Content-Type": "text/plain", - }, - "data": "This is expected to be sent back as part of response body.", - }, - "validate": [{"eq": ["status_code", 200]}], - } + Step( + RunRequest("post raw text") + .post("/post") + .with_headers( + **{"User-Agent": "HttpRunner/3.0", "Content-Type": "text/plain"} + ) + .with_data("This is expected to be sent back as part of response body.") + .validate() + .assert_equal("status_code", 200) ), - TStep( - **{ - "name": "post form data", - "request": { - "method": "POST", - "url": "/post", - "headers": { - "User-Agent": "HttpRunner/3.0", - "Content-Type": "application/x-www-form-urlencoded", - }, - "data": "foo1=bar1&foo2=bar2", - }, - "validate": [{"eq": ["status_code", 200]}], - } + Step( + RunRequest("post form data") + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/3.0", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=bar1&foo2=bar2") + .validate() + .assert_equal("status_code", 200) ), - TStep( - **{ - "name": "put request", - "request": { - "method": "PUT", - "url": "/put", - "headers": { - "User-Agent": "HttpRunner/3.0", - "Content-Type": "text/plain", - }, - "data": "This is expected to be sent back as part of response body.", - }, - "validate": [{"eq": ["status_code", 200]}], - } + Step( + RunRequest("put request") + .put("/put") + .with_headers( + **{"User-Agent": "HttpRunner/3.0", "Content-Type": "text/plain"} + ) + .with_data("This is expected to be sent back as part of response body.") + .validate() + .assert_equal("status_code", 200) ), ] 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 e084d808..b8f333bb 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -1,67 +1,88 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/postman_echo/request_methods/request_with_functions.yml -from httprunner import HttpRunner, Config, Step, Request +from httprunner import HttpRunner, TConfig, TStep class TestCaseRequestWithFunctions(HttpRunner): - config = ( - Config("request methods testcase with functions") - .variables(foo1="session_bar1") - .base_url("https://postman-echo.com") - .verify(False) + config = TConfig( + **{ + "name": "request methods testcase with functions", + "variables": {"foo1": "session_bar1"}, + "base_url": "https://postman-echo.com", + "verify": False, + "path": "examples/postman_echo/request_methods/request_with_functions_test.py", + } ) teststeps = [ - Step("get with params") - .with_variables(foo1="bar1", foo2="session_bar2", sum_v="${sum_two(1, 2)}") - .set_extractor("session_foo2", "body.args.foo2") - .run_request( - Request() - .get("/get") - .with_params(foo1="$foo1", foo2="$foo2", sum_v="$sum_v") - .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) - ) - .assert_equal("status_code", 200) - .assert_equal("body.args.foo1", "session_bar1") - .assert_equal("body.args.sum_v", "3") - .assert_equal("body.args.foo2", "session_bar2"), - Step("post raw text") - .with_variables(foo1="hello world", foo3="$session_foo2") - .run_request( - Request() - .post("/post") - .with_headers( - **{ - "User-Agent": "HttpRunner/${get_httprunner_version()}", - "Content-Type": "text/plain", - } - ) - .with_data( - "This is expected to be sent back as part of response body: $foo1-$foo3." - ) - ) - .assert_equal("status_code", 200) - .assert_equal( - "body.data", - "This is expected to be sent back as part of response body: session_bar1-session_bar2.", + 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"]}, + ], + } ), - Step("post form data") - .with_variables(**{"foo1": "bar1", "foo2": "bar2"}) - .run_request( - Request() - .post("/post") - .with_headers( - **{ - "User-Agent": "HttpRunner/${get_httprunner_version()}", - "Content-Type": "application/x-www-form-urlencoded", - } - ) - .with_data("foo1=$foo1&foo2=$foo2") - ) - .assert_equal("status_code", 200) - .assert_equal("body.form.foo1", "session_bar1") - .assert_equal("body.form.foo2", "bar2"), ] 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 cb2fc35a..5d8ae4f2 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 @@ -6,7 +6,7 @@ import sys sys.path.insert(0, os.getcwd()) -from httprunner import HttpRunner, Config, Step +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from examples.postman_echo.request_methods.request_with_functions_test import ( TestCaseRequestWithFunctions as RequestWithFunctions, @@ -16,15 +16,17 @@ from examples.postman_echo.request_methods.request_with_functions_test import ( class TestCaseRequestWithTestcaseReference(HttpRunner): config = ( Config("request methods testcase: reference testcase") - .variables(foo1="session_bar1") + .variables(**{"foo1": "session_bar1"}) .base_url("https://postman-echo.com") .verify(False) ) teststeps = [ - Step("request with functions") - .with_variables(foo1="override_bar1") - .run_testcase(RequestWithFunctions), + Step( + RunTestCase("request with functions") + .with_variables(**{"foo1": "override_bar1"}) + .call(RequestWithFunctions) + ), ] 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 6460eb90..d7fc5241 100644 --- a/examples/postman_echo/request_methods/request_with_variables_test.py +++ b/examples/postman_echo/request_methods/request_with_variables_test.py @@ -1,82 +1,63 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/postman_echo/request_methods/request_with_variables.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseRequestWithVariables(HttpRunner): - config = TConfig( - **{ - "name": "request methods testcase with variables", - "variables": {"foo1": "session_bar1"}, - "base_url": "https://postman-echo.com", - "verify": False, - "path": "examples/postman_echo/request_methods/request_with_variables_test.py", - } + config = ( + Config("request methods testcase with variables") + .variables(**{"foo1": "session_bar1"}) + .base_url("https://postman-echo.com") + .verify(False) ) teststeps = [ - TStep( - **{ - "name": "get with params", - "variables": {"foo1": "bar1", "foo2": "session_bar2"}, - "request": { - "method": "GET", - "url": "/get", - "params": {"foo1": "$foo1", "foo2": "$foo2"}, - "headers": {"User-Agent": "HttpRunner/3.0"}, - }, - "extract": {"session_foo2": "body.args.foo2"}, - "validate": [ - {"eq": ["status_code", 200]}, - {"eq": ["body.args.foo1", "session_bar1"]}, - {"eq": ["body.args.foo2", "session_bar2"]}, - ], - } + Step( + RunRequest("get with params") + .with_variables(**{"foo1": "bar1", "foo2": "session_bar2"}) + .get("/get") + .with_params(**{"foo1": "$foo1", "foo2": "$foo2"}) + .with_headers(**{"User-Agent": "HttpRunner/3.0"}) + .extract() + .with_jmespath("body.args.foo2", "session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.foo1", "session_bar1") + .assert_equal("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/3.0", - "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.", - ] - }, - ], - } + Step( + RunRequest("post raw text") + .with_variables(**{"foo1": "hello world", "foo3": "$session_foo2"}) + .post("/post") + .with_headers( + **{"User-Agent": "HttpRunner/3.0", "Content-Type": "text/plain"} + ) + .with_data( + "This is expected to be sent back as part of response body: $foo1-$foo3." + ) + .validate() + .assert_equal("status_code", 200) + .assert_equal( + "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/3.0", - "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"]}, - ], - } + Step( + RunRequest("post form data") + .with_variables(**{"foo1": "bar1", "foo2": "bar2"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/3.0", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "session_bar1") + .assert_equal("body.form.foo2", "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 1346ec8e..6d39269d 100644 --- a/examples/postman_echo/request_methods/validate_with_functions_test.py +++ b/examples/postman_echo/request_methods/validate_with_functions_test.py @@ -1,41 +1,31 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/postman_echo/request_methods/validate_with_functions.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseValidateWithFunctions(HttpRunner): - config = TConfig( - **{ - "name": "request methods testcase: validate with functions", - "variables": {"foo1": "session_bar1"}, - "base_url": "https://postman-echo.com", - "verify": False, - "path": "examples/postman_echo/request_methods/validate_with_functions_test.py", - } + config = ( + Config("request methods testcase: validate with functions") + .variables(**{"foo1": "session_bar1"}) + .base_url("https://postman-echo.com") + .verify(False) ) 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.sum_v", "3"]}, - ], - } + Step( + RunRequest("get with params") + .with_variables( + **{"foo1": "bar1", "foo2": "session_bar2", "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.foo2", "session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.sum_v", "3") ), ] 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 1cb75fa9..2ae99691 100644 --- a/examples/postman_echo/request_methods/validate_with_variables_test.py +++ b/examples/postman_echo/request_methods/validate_with_variables_test.py @@ -1,82 +1,63 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/postman_echo/request_methods/validate_with_variables.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseValidateWithVariables(HttpRunner): - config = TConfig( - **{ - "name": "request methods testcase: validate with variables", - "variables": {"foo1": "session_bar1"}, - "base_url": "https://postman-echo.com", - "verify": False, - "path": "examples/postman_echo/request_methods/validate_with_variables_test.py", - } + config = ( + Config("request methods testcase: validate with variables") + .variables(**{"foo1": "session_bar1"}) + .base_url("https://postman-echo.com") + .verify(False) ) teststeps = [ - TStep( - **{ - "name": "get with params", - "variables": {"foo1": "bar1", "foo2": "session_bar2"}, - "request": { - "method": "GET", - "url": "/get", - "params": {"foo1": "$foo1", "foo2": "$foo2"}, - "headers": {"User-Agent": "HttpRunner/3.0"}, - }, - "extract": {"session_foo2": "body.args.foo2"}, - "validate": [ - {"eq": ["status_code", 200]}, - {"eq": ["body.args.foo1", "$foo1"]}, - {"eq": ["body.args.foo2", "$foo2"]}, - ], - } + Step( + RunRequest("get with params") + .with_variables(**{"foo1": "bar1", "foo2": "session_bar2"}) + .get("/get") + .with_params(**{"foo1": "$foo1", "foo2": "$foo2"}) + .with_headers(**{"User-Agent": "HttpRunner/3.0"}) + .extract() + .with_jmespath("body.args.foo2", "session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.foo1", "$foo1") + .assert_equal("body.args.foo2", "$foo2") ), - TStep( - **{ - "name": "post raw text", - "variables": {"foo1": "hello world", "foo3": "$session_foo2"}, - "request": { - "method": "POST", - "url": "/post", - "headers": { - "User-Agent": "HttpRunner/3.0", - "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-$foo3.", - ] - }, - ], - } + Step( + RunRequest("post raw text") + .with_variables(**{"foo1": "hello world", "foo3": "$session_foo2"}) + .post("/post") + .with_headers( + **{"User-Agent": "HttpRunner/3.0", "Content-Type": "text/plain"} + ) + .with_data( + "This is expected to be sent back as part of response body: $foo1-$foo3." + ) + .validate() + .assert_equal("status_code", 200) + .assert_equal( + "body.data", + "This is expected to be sent back as part of response body: session_bar1-$foo3.", + ) ), - TStep( - **{ - "name": "post form data", - "variables": {"foo1": "bar1", "foo2": "bar2"}, - "request": { - "method": "POST", - "url": "/post", - "headers": { - "User-Agent": "HttpRunner/3.0", - "Content-Type": "application/x-www-form-urlencoded", - }, - "data": "foo1=$foo1&foo2=$foo2", - }, - "validate": [ - {"eq": ["status_code", 200]}, - {"eq": ["body.form.foo1", "$foo1"]}, - {"eq": ["body.form.foo2", "$foo2"]}, - ], - } + Step( + RunRequest("post form data") + .with_variables(**{"foo1": "bar1", "foo2": "bar2"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/3.0", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "$foo1") + .assert_equal("body.form.foo2", "$foo2") ), ] diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 58134ded..71d2df85 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -3,7 +3,7 @@ __description__ = "One-stop solution for HTTP(S) testing." from httprunner.runner import HttpRunner from httprunner.schema import TConfig, TStep -from httprunner.testcase import Config, Step, Request +from httprunner.testcase import Config, Step, RunRequest, RunTestCase __all__ = [ "__version__", @@ -13,5 +13,6 @@ __all__ = [ "TStep", "Config", "Step", - "Request", + "RunRequest", + "RunTestCase", ] diff --git a/httprunner/cli.py b/httprunner/cli.py index 5297074d..762ae2a7 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -23,9 +23,14 @@ def main_run(extra_args): # keep compatibility with v2 extra_args = ensure_cli_args(extra_args) + chain_style = False tests_path_list = [] extra_args_new = [] for item in extra_args: + if item == "--chain-style": + chain_style = True + continue + if not os.path.exists(item): # item is not file/folder path extra_args_new.append(item) @@ -38,7 +43,7 @@ def main_run(extra_args): logger.error(f"No valid testcase path in cli arguments: {extra_args}") sys.exit(1) - testcase_path_list = main_make(tests_path_list) + testcase_path_list = main_make(tests_path_list, chain_style=chain_style) if not testcase_path_list: logger.error("No valid testcases found, exit 1.") sys.exit(1) @@ -110,7 +115,7 @@ def main(): elif sys.argv[1] == "har2case": main_har2case(args) elif sys.argv[1] == "make": - main_make(args.testcase_path) + main_make(args.testcase_path, chain_style=args.chain_style) def main_hrun_alias(): diff --git a/httprunner/make.py b/httprunner/make.py index 0d19aacd..a2ca8066 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -16,6 +16,7 @@ from httprunner.loader import ( load_project_meta, ) from httprunner.parser import parse_data +from httprunner.response import uniform_validator """ cache converted pytest files, avoid duplicate making """ @@ -50,6 +51,35 @@ if __name__ == "__main__": """ ) +__TEMPLATE_CHAIN_STYLE__ = jinja2.Template( + """# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# FROM: {{ testcase_path }} +{% if imports_list %} +import os +import sys + +sys.path.insert(0, os.getcwd()) +{% endif %} +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase +{% for import_str in imports_list %} +{{ import_str }} +{% endfor %} + +class {{ class_name }}(HttpRunner): + config = {{ config_chain_style }} + + teststeps = [ + {% for step_chain_style in teststeps_chain_style %} + {{ step_chain_style }}, + {% endfor %} + ] + +if __name__ == "__main__": + {{ class_name }}().test_start() + +""" +) + def __ensure_file_name(path: Text) -> Text: """ ensure file name not startswith digit @@ -96,7 +126,7 @@ def __ensure_testcase_module(path: Text) -> NoReturn: return with open(init_file, "w", encoding="utf-8") as f: - f.write("# NOTICE: Generated By HttpRunner. DO NOT EDIT!") + f.write("# NOTICE: Generated By HttpRunner. DO NOT EDIT!\n") def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]: @@ -132,8 +162,105 @@ def __format_pytest_with_black(python_paths: List[Text]) -> NoReturn: logger.error(ex) +def make_config_chain_style(config: Dict) -> Text: + config_chain_style = f'Config("{config["name"]}")' + + if config["variables"]: + variables = config["variables"] + config_chain_style += f".variables(**{variables})" + + if "base_url" in config: + config_chain_style += f'.base_url("{config["base_url"]}")' + + if "verify" in config: + config_chain_style += f'.verify({config["verify"]})' + + return config_chain_style + + +def make_request_chain_style(request: Dict) -> Text: + method = request["method"].lower() + url = request["url"] + request_chain_style = f'.{method}("{url}")' + + if "params" in request: + params = request["params"] + request_chain_style += f".with_params(**{params})" + + if "headers" in request: + headers = request["headers"] + request_chain_style += f".with_headers(**{headers})" + + if "cookies" in request: + cookies = request["cookies"] + request_chain_style += f".with_cookies(**{cookies})" + + if "data" in request: + data = request["data"] + if isinstance(data, Text): + data = f'"{data}"' + request_chain_style += f".with_data({data})" + + if "timeout" in request: + timeout = request["timeout"] + request_chain_style += f".set_timeout({timeout})" + + if "verify" in request: + verify = request["verify"] + request_chain_style += f".set_verify({verify})" + + if "allow_redirects" in request: + allow_redirects = request["allow_redirects"] + request_chain_style += f".set_allow_redirects({allow_redirects})" + + return request_chain_style + + +def make_teststep_chain_style(teststep: Dict) -> Text: + if teststep.get("request"): + step_info = f'RunRequest("{teststep["name"]}")' + elif teststep.get("testcase"): + step_info = f'RunTestCase("{teststep["name"]}")' + else: + raise exceptions.TestCaseFormatError + + if "variables" in teststep: + variables = teststep["variables"] + step_info += f".with_variables(**{variables})" + + if teststep.get("request"): + step_info += make_request_chain_style(teststep["request"]) + elif teststep.get("testcase"): + testcase = teststep["testcase"].replace("CLS_LB(", "").replace(")CLS_RB", "") + call_ref_testcase = f".call({testcase})" + step_info += call_ref_testcase + + if "extract" in teststep: + step_info += ".extract()" + + for extract_name, extract_path in teststep["extract"].items(): + step_info += f'.with_jmespath("{extract_path}", "{extract_name}")' + + if "validate" in teststep: + step_info += ".validate()" + + for v in teststep["validate"]: + validator = uniform_validator(v) + assert_method = validator["assert"] + check = validator["check"] + expect = validator["expect"] + if isinstance(expect, Text): + expect = f'"{expect}"' + step_info += f'.assert_{assert_method}("{check}", {expect})' + + return f"Step({step_info})" + + def __make_testcase( - testcase: Dict, dir_path: Text = None, ref_flag: bool = False + testcase: Dict, + dir_path: Text = None, + ref_flag: bool = False, + chain_style: bool = False, ) -> NoReturn: """convert valid testcase dict to pytest file path""" # ensure compatibility with testcase format v2 @@ -195,12 +322,20 @@ def __make_testcase( data = { "testcase_path": __ensure_cwd_relative(testcase_path), "class_name": f"TestCase{testcase_cls_name}", - "config": config, - "teststeps": teststeps, "imports_list": imports_list, } - content = __TEMPLATE__.render(data) - content = content.replace("'CLS_LB(", "").replace(")CLS_RB'", "") + + if chain_style: + data["config_chain_style"] = make_config_chain_style(config) + data["teststeps_chain_style"] = [ + make_teststep_chain_style(step) for step in teststeps + ] + content = __TEMPLATE_CHAIN_STYLE__.render(data) + else: + data["config"] = config + data["teststeps"] = teststeps + content = __TEMPLATE__.render(data) + content = content.replace("'CLS_LB(", "").replace(")CLS_RB'", "") with open(testcase_python_path, "w", encoding="utf-8") as f: f.write(content) @@ -213,7 +348,7 @@ def __make_testcase( make_files_cache_set.add(__ensure_cwd_relative(testcase_python_path)) -def __make_testsuite(testsuite: Dict) -> NoReturn: +def __make_testsuite(testsuite: Dict, chain_style: bool = False) -> NoReturn: """convert valid testsuite dict to pytest folder with testcases""" # validate testsuite format load_testsuite(testsuite) @@ -258,10 +393,12 @@ def __make_testsuite(testsuite: Dict) -> NoReturn: testcase_dict["config"]["variables"].update(testsuite_variables) # make testcase - __make_testcase(testcase_dict, testsuite_dir) + __make_testcase(testcase_dict, testsuite_dir, chain_style=chain_style) -def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn: +def __make( + tests_path: Text, ref_flag: bool = False, chain_style: bool = False +) -> NoReturn: """ make testcase(s) with testcase/testsuite/folder absolute path generated pytest file path will be cached in make_files_cache_set @@ -295,14 +432,16 @@ def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn: # testcase if "teststeps" in test_content: try: - __make_testcase(test_content, ref_flag=ref_flag) + __make_testcase( + test_content, ref_flag=ref_flag, chain_style=chain_style + ) except exceptions.TestCaseFormatError: continue # testsuite elif "testcases" in test_content: try: - __make_testsuite(test_content) + __make_testsuite(test_content, chain_style=chain_style) except exceptions.TestSuiteFormatError: continue @@ -313,12 +452,12 @@ def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn: ) -def main_make(tests_paths: List[Text]) -> List[Text]: +def main_make(tests_paths: List[Text], chain_style: bool = False) -> List[Text]: for tests_path in tests_paths: if not os.path.isabs(tests_path): tests_path = os.path.join(os.getcwd(), tests_path) - __make(tests_path) + __make(tests_path, chain_style=chain_style) testcase_path_list = list(make_files_cache_set) __format_pytest_with_black(testcase_path_list) @@ -334,5 +473,11 @@ def init_make_parser(subparsers): parser.add_argument( "testcase_path", nargs="*", help="Specify YAML/JSON testcase file/folder path" ) + parser.add_argument( + "--chain-style", + dest="chain_style", + action="store_true", + help="Convert pytest files in chain-style.", + ) return parser diff --git a/httprunner/runner.py b/httprunner/runner.py index 9a1c4774..b2c9060f 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -20,7 +20,7 @@ from httprunner.ext.uploader import prepare_upload_step from httprunner.loader import load_project_meta, load_testcase_file from httprunner.parser import build_url, parse_data, parse_variables_mapping from httprunner.response import ResponseObject -from httprunner.testcase import Config, StepValidation +from httprunner.testcase import Config, Step from httprunner.schema import ( TConfig, TStep, @@ -36,7 +36,7 @@ from httprunner.schema import ( class HttpRunner(object): config: Union[TConfig, Config] - teststeps: List[Union[TStep, StepValidation]] + teststeps: List[Union[TStep, Step]] success: bool = True # indicate testcase execution result __config: TConfig @@ -64,7 +64,7 @@ class HttpRunner(object): for step in self.teststeps: if isinstance(step, TStep): self.__teststeps.append(step) - elif isinstance(step, StepValidation): + elif isinstance(step, Step): self.__teststeps.append(step.perform()) else: raise exceptions.TestCaseFormatError(f"step type error: {step}") diff --git a/httprunner/testcase.py b/httprunner/testcase.py index a2af4c4a..1f772a2a 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -1,5 +1,5 @@ import inspect -from typing import Text, Any, Dict, Callable +from typing import Text, Any, Union, Callable from httprunner.schema import ( TConfig, @@ -50,154 +50,166 @@ class Config(object): ) -class RequestWithOptionalArgs(object): - def __init__(self, method: MethodEnum, url: Text): - self.__method = method - self.__url = url - self.__params = {} - self.__headers = {} - self.__cookies = {} - self.__data = "" - self.__timeout = 120 - self.__allow_redirects = True - self.__verify = False - - def with_params(self, **params) -> "RequestWithOptionalArgs": - self.__params.update(params) - return self - - def with_headers(self, **headers) -> "RequestWithOptionalArgs": - self.__headers.update(headers) - return self - - def with_cookies(self, **cookies) -> "RequestWithOptionalArgs": - self.__cookies.update(cookies) - return self - - def with_data(self, data) -> "RequestWithOptionalArgs": - self.__data = data - return self - - def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs": - self.__timeout = timeout - return self - - def set_verify(self, verify: bool) -> "RequestWithOptionalArgs": - self.__verify = verify - return self - - def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs": - self.__allow_redirects = allow_redirects - return self - - def perform(self) -> TRequest: - """build TRequest object with configs""" - return TRequest( - method=self.__method, - url=self.__url, - params=self.__params, - headers=self.__headers, - data=self.__data, - timeout=self.__timeout, - verify=self.__verify, - allow_redirects=self.__allow_redirects, - ) - - -class Request(object): - def get(self, url: Text) -> RequestWithOptionalArgs: - return RequestWithOptionalArgs(MethodEnum.GET, url) - - def post(self, url: Text) -> RequestWithOptionalArgs: - return RequestWithOptionalArgs(MethodEnum.POST, url) - - def put(self, url: Text) -> RequestWithOptionalArgs: - return RequestWithOptionalArgs(MethodEnum.PUT, url) - - def head(self, url: Text) -> RequestWithOptionalArgs: - return RequestWithOptionalArgs(MethodEnum.HEAD, url) - - def delete(self, url: Text) -> RequestWithOptionalArgs: - return RequestWithOptionalArgs(MethodEnum.DELETE, url) - - def options(self, url: Text) -> RequestWithOptionalArgs: - return RequestWithOptionalArgs(MethodEnum.OPTIONS, url) - - def patch(self, url: Text) -> RequestWithOptionalArgs: - return RequestWithOptionalArgs(MethodEnum.PATCH, url) - - class StepValidation(object): - def __init__( - self, - name: Text, - variables: Dict, - extractors: Dict, - request: TRequest = None, - testcase: Callable = None, - ): - self.__name = name - self.__variables = variables - self.__extractors = extractors - self.__request: TRequest = request - self.__testcase: Callable = testcase - self.__validators = [] - - @property - def request(self) -> TRequest: - return self.__request - - @property - def testcase(self) -> TestCase: - return self.__testcase + def __init__(self, step: TStep): + self.__t_step = step def assert_equal(self, jmes_path: Text, expected_value: Any) -> "StepValidation": - self.__validators.append({"eq": [jmes_path, expected_value]}) + self.__t_step.validators.append({"eq": [jmes_path, expected_value]}) return self def assert_greater_than( self, jmes_path: Text, expected_value: Any ) -> "StepValidation": - self.__validators.append({"gt": [jmes_path, expected_value]}) + self.__t_step.validators.append({"gt": [jmes_path, expected_value]}) return self def assert_less_than( self, jmes_path: Text, expected_value: Any ) -> "StepValidation": - self.__validators.append({"lt": [jmes_path, expected_value]}) + self.__t_step.validators.append({"lt": [jmes_path, expected_value]}) return self def perform(self) -> TStep: - return TStep( - name=self.__name, - variables=self.__variables, - request=self.__request, - testcase=self.__testcase, - extract=self.__extractors, - validate=self.__validators, - ) + return self.__t_step + + +class StepExtraction(object): + def __init__(self, step: TStep): + self.__t_step = step + + def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepExtraction": + self.__t_step.extract[var_name] = jmes_path + return self + + # def with_regex(self): + # # TODO: extract response html with regex + # pass + # + # def with_jsonpath(self): + # # TODO: extract response json with jsonpath + # pass + + def validate(self) -> StepValidation: + return StepValidation(self.__t_step) + + def perform(self) -> TStep: + return self.__t_step + + +class RequestWithOptionalArgs(object): + def __init__(self, step: TStep): + self.__t_step = step + + def with_params(self, **params) -> "RequestWithOptionalArgs": + self.__t_step.request.params.update(params) + return self + + def with_headers(self, **headers) -> "RequestWithOptionalArgs": + self.__t_step.request.headers.update(headers) + return self + + def with_cookies(self, **cookies) -> "RequestWithOptionalArgs": + self.__t_step.request.cookies.update(cookies) + return self + + def with_data(self, data) -> "RequestWithOptionalArgs": + self.__t_step.request.data = data + return self + + def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs": + self.__t_step.request.timeout = timeout + return self + + def set_verify(self, verify: bool) -> "RequestWithOptionalArgs": + self.__t_step.request.verify = verify + return self + + def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs": + self.__t_step.request.allow_redirects = allow_redirects + return self + + # def hooks(self): + # pass + + def extract(self) -> StepExtraction: + return StepExtraction(self.__t_step) + + def validate(self) -> StepValidation: + return StepValidation(self.__t_step) + + def perform(self) -> TStep: + return self.__t_step + + +class RunRequest(object): + def __init__(self, name: Text): + self.__t_step = TStep(name=name) + + def with_variables(self, **variables) -> "RunRequest": + self.__t_step.variables.update(variables) + return self + + def get(self, url: Text) -> RequestWithOptionalArgs: + self.__t_step.request = TRequest(method=MethodEnum.GET, url=url) + return RequestWithOptionalArgs(self.__t_step) + + def post(self, url: Text) -> RequestWithOptionalArgs: + self.__t_step.request = TRequest(method=MethodEnum.POST, url=url) + return RequestWithOptionalArgs(self.__t_step) + + def put(self, url: Text) -> RequestWithOptionalArgs: + self.__t_step.request = TRequest(method=MethodEnum.PUT, url=url) + return RequestWithOptionalArgs(self.__t_step) + + def head(self, url: Text) -> RequestWithOptionalArgs: + self.__t_step.request = TRequest(method=MethodEnum.HEAD, url=url) + return RequestWithOptionalArgs(self.__t_step) + + def delete(self, url: Text) -> RequestWithOptionalArgs: + self.__t_step.request = TRequest(method=MethodEnum.DELETE, url=url) + return RequestWithOptionalArgs(self.__t_step) + + def options(self, url: Text) -> RequestWithOptionalArgs: + self.__t_step.request = TRequest(method=MethodEnum.OPTIONS, url=url) + return RequestWithOptionalArgs(self.__t_step) + + def patch(self, url: Text) -> RequestWithOptionalArgs: + self.__t_step.request = TRequest(method=MethodEnum.PATCH, url=url) + return RequestWithOptionalArgs(self.__t_step) + + +class RunTestCase(object): + def __init__(self, name: Text): + self.__t_step = TStep(name=name) + + def with_variables(self, **variables) -> "RunTestCase": + self.__t_step.variables.update(variables) + return self + + def call(self, testcase: Callable): + self.__t_step.testcase = testcase + + def perform(self) -> TStep: + return self.__t_step class Step(object): - def __init__(self, name: Text): - self.__name = name - self.__variables = {} - self.__extractors = {} + def __init__( + self, + step: Union[ + StepValidation, StepExtraction, RequestWithOptionalArgs, RunTestCase + ], + ): + self.__t_step = step.perform() - def with_variables(self, **variables) -> "Step": - self.__variables.update(variables) - return self + @property + def request(self) -> TRequest: + return self.__t_step.request - def set_extractor(self, var_name: Text, jmes_path: Text) -> "Step": - self.__extractors[var_name] = jmes_path - return self + @property + def testcase(self) -> TestCase: + return self.__t_step.testcase - def run_request(self, req_obj: RequestWithOptionalArgs) -> "StepValidation": - return StepValidation( - self.__name, self.__variables, self.__extractors, request=req_obj.perform() - ) - - def run_testcase(self, testcase: Callable) -> "StepValidation": - return StepValidation( - self.__name, self.__variables, self.__extractors, testcase=testcase - ) + def perform(self) -> TStep: + return self.__t_step diff --git a/tests/make_test.py b/tests/make_test.py index 7884910e..703f5f10 100644 --- a/tests/make_test.py +++ b/tests/make_test.py @@ -1,6 +1,12 @@ import unittest -from httprunner.make import main_make, convert_testcase_path, make_files_cache_set +from httprunner.make import ( + main_make, + convert_testcase_path, + make_files_cache_set, + make_config_chain_style, + make_teststep_chain_style, +) class TestLoader(unittest.TestCase): @@ -95,3 +101,42 @@ from examples.postman_echo.request_methods.request_with_functions_test import ( "examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py", testcase_python_list, ) + + def test_make_config_chain_style(self): + config = { + "name": "request methods testcase: validate with functions", + "variables": {"foo1": "bar1", "foo2": 22}, + "base_url": "https://postman-echo.com", + "verify": False, + "path": "examples/postman_echo/request_methods/validate_with_functions_test.py", + } + self.assertEqual( + make_config_chain_style(config), + """Config("request methods testcase: validate with functions").variables(**{'foo1': 'bar1', 'foo2': 22}).base_url("https://postman-echo.com").verify(False)""", + ) + + def test_make_teststep_chain_style(self): + step = { + "name": "get with params", + "variables": {"foo1": "bar1", "foo2": 123, "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()}"}, + }, + "testcase": "CLS_LB(TestCaseDemo)CLS_RB", + "extract": { + "session_foo1": "body.args.foo1", + "session_foo2": "body.args.foo2", + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.args.sum_v", "3"]}, + ], + } + teststep_chain_style = make_teststep_chain_style(step) + self.assertEqual( + 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"))""", + ) From 59e99eeaa8f043b8f4dfaacef6581ece0ed890b5 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 15:49:20 +0800 Subject: [PATCH 19/32] change: generate pytest in chain style by default --- .../request_with_functions_test.py | 128 ++++++++---------- httprunner/cli.py | 9 +- httprunner/make.py | 86 +++--------- httprunner/runner.py | 21 +-- httprunner/scaffold.py | 2 +- httprunner/testcase.py | 1 + tests/make_test.py | 2 +- 7 files changed, 86 insertions(+), 163 deletions(-) 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 b8f333bb..ff7180aa 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -1,87 +1,69 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/postman_echo/request_methods/request_with_functions.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseRequestWithFunctions(HttpRunner): - config = TConfig( - **{ - "name": "request methods testcase with functions", - "variables": {"foo1": "session_bar1"}, - "base_url": "https://postman-echo.com", - "verify": False, - "path": "examples/postman_echo/request_methods/request_with_functions_test.py", - } + config = ( + Config("request methods testcase with functions") + .variables(**{"foo1": "session_bar1"}) + .base_url("https://postman-echo.com") + .verify(False) ) 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"]}, - ], - } + Step( + RunRequest("get with params") + .with_variables( + **{"foo1": "bar1", "foo2": "session_bar2", "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.foo2", "session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.foo1", "session_bar1") + .assert_equal("body.args.sum_v", "3") + .assert_equal("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.", - ] - }, - ], - } + Step( + RunRequest("post raw text") + .with_variables(**{"foo1": "hello world", "foo3": "$session_foo2"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "text/plain", + } + ) + .with_data( + "This is expected to be sent back as part of response body: $foo1-$foo3." + ) + .validate() + .assert_equal("status_code", 200) + .assert_equal( + "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"]}, - ], - } + Step( + RunRequest("post form data") + .with_variables(**{"foo1": "bar1", "foo2": "bar2"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "session_bar1") + .assert_equal("body.form.foo2", "bar2") ), ] diff --git a/httprunner/cli.py b/httprunner/cli.py index 762ae2a7..5297074d 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -23,14 +23,9 @@ def main_run(extra_args): # keep compatibility with v2 extra_args = ensure_cli_args(extra_args) - chain_style = False tests_path_list = [] extra_args_new = [] for item in extra_args: - if item == "--chain-style": - chain_style = True - continue - if not os.path.exists(item): # item is not file/folder path extra_args_new.append(item) @@ -43,7 +38,7 @@ def main_run(extra_args): logger.error(f"No valid testcase path in cli arguments: {extra_args}") sys.exit(1) - testcase_path_list = main_make(tests_path_list, chain_style=chain_style) + testcase_path_list = main_make(tests_path_list) if not testcase_path_list: logger.error("No valid testcases found, exit 1.") sys.exit(1) @@ -115,7 +110,7 @@ def main(): elif sys.argv[1] == "har2case": main_har2case(args) elif sys.argv[1] == "make": - main_make(args.testcase_path, chain_style=args.chain_style) + main_make(args.testcase_path) def main_hrun_alias(): diff --git a/httprunner/make.py b/httprunner/make.py index a2ca8066..94dfa6aa 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -29,35 +29,6 @@ __TEMPLATE__ = jinja2.Template( import os import sys -sys.path.insert(0, os.getcwd()) -{% endif %} -from httprunner import HttpRunner, TConfig, TStep -{% for import_str in imports_list %} -{{ import_str }} -{% endfor %} - -class {{ class_name }}(HttpRunner): - config = TConfig(**{{ config }}) - - teststeps = [ - {% for teststep in teststeps %} - TStep(**{{ teststep }}), - {% endfor %} - ] - -if __name__ == "__main__": - {{ class_name }}().test_start() - -""" -) - -__TEMPLATE_CHAIN_STYLE__ = jinja2.Template( - """# NOTICE: Generated By HttpRunner. DO NOT EDIT! -# FROM: {{ testcase_path }} -{% if imports_list %} -import os -import sys - sys.path.insert(0, os.getcwd()) {% endif %} from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase @@ -231,7 +202,7 @@ def make_teststep_chain_style(teststep: Dict) -> Text: if teststep.get("request"): step_info += make_request_chain_style(teststep["request"]) elif teststep.get("testcase"): - testcase = teststep["testcase"].replace("CLS_LB(", "").replace(")CLS_RB", "") + testcase = teststep["testcase"] call_ref_testcase = f".call({testcase})" step_info += call_ref_testcase @@ -248,19 +219,21 @@ def make_teststep_chain_style(teststep: Dict) -> Text: validator = uniform_validator(v) assert_method = validator["assert"] check = validator["check"] + if '"' in check: + # e.g. body."user-agent" => 'body."user-agent"' + check = f"'{check}'" + else: + check = f'"{check}"' expect = validator["expect"] if isinstance(expect, Text): expect = f'"{expect}"' - step_info += f'.assert_{assert_method}("{check}", {expect})' + step_info += f'.assert_{assert_method}({check}, {expect})' return f"Step({step_info})" def __make_testcase( - testcase: Dict, - dir_path: Text = None, - ref_flag: bool = False, - chain_style: bool = False, + testcase: Dict, dir_path: Text = None, ref_flag: bool = False, ) -> NoReturn: """convert valid testcase dict to pytest file path""" # ensure compatibility with testcase format v2 @@ -309,7 +282,7 @@ def __make_testcase( ref_testcase_python_path, ref_testcase_cls_name = convert_testcase_path( ref_testcase_path ) - teststep["testcase"] = f"CLS_LB({ref_testcase_cls_name})CLS_RB" + teststep["testcase"] = ref_testcase_cls_name # prepare import ref testcase ref_testcase_python_path = ref_testcase_python_path[len(os.getcwd()) + 1 :] @@ -323,19 +296,12 @@ def __make_testcase( "testcase_path": __ensure_cwd_relative(testcase_path), "class_name": f"TestCase{testcase_cls_name}", "imports_list": imports_list, - } - - if chain_style: - data["config_chain_style"] = make_config_chain_style(config) - data["teststeps_chain_style"] = [ + "config_chain_style": make_config_chain_style(config), + "teststeps_chain_style": [ make_teststep_chain_style(step) for step in teststeps - ] - content = __TEMPLATE_CHAIN_STYLE__.render(data) - else: - data["config"] = config - data["teststeps"] = teststeps - content = __TEMPLATE__.render(data) - content = content.replace("'CLS_LB(", "").replace(")CLS_RB'", "") + ], + } + content = __TEMPLATE__.render(data) with open(testcase_python_path, "w", encoding="utf-8") as f: f.write(content) @@ -348,7 +314,7 @@ def __make_testcase( make_files_cache_set.add(__ensure_cwd_relative(testcase_python_path)) -def __make_testsuite(testsuite: Dict, chain_style: bool = False) -> NoReturn: +def __make_testsuite(testsuite: Dict) -> NoReturn: """convert valid testsuite dict to pytest folder with testcases""" # validate testsuite format load_testsuite(testsuite) @@ -393,12 +359,10 @@ def __make_testsuite(testsuite: Dict, chain_style: bool = False) -> NoReturn: testcase_dict["config"]["variables"].update(testsuite_variables) # make testcase - __make_testcase(testcase_dict, testsuite_dir, chain_style=chain_style) + __make_testcase(testcase_dict, testsuite_dir) -def __make( - tests_path: Text, ref_flag: bool = False, chain_style: bool = False -) -> NoReturn: +def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn: """ make testcase(s) with testcase/testsuite/folder absolute path generated pytest file path will be cached in make_files_cache_set @@ -432,16 +396,14 @@ def __make( # testcase if "teststeps" in test_content: try: - __make_testcase( - test_content, ref_flag=ref_flag, chain_style=chain_style - ) + __make_testcase(test_content, ref_flag=ref_flag) except exceptions.TestCaseFormatError: continue # testsuite elif "testcases" in test_content: try: - __make_testsuite(test_content, chain_style=chain_style) + __make_testsuite(test_content) except exceptions.TestSuiteFormatError: continue @@ -452,12 +414,12 @@ def __make( ) -def main_make(tests_paths: List[Text], chain_style: bool = False) -> List[Text]: +def main_make(tests_paths: List[Text]) -> List[Text]: for tests_path in tests_paths: if not os.path.isabs(tests_path): tests_path = os.path.join(os.getcwd(), tests_path) - __make(tests_path, chain_style=chain_style) + __make(tests_path) testcase_path_list = list(make_files_cache_set) __format_pytest_with_black(testcase_path_list) @@ -473,11 +435,5 @@ def init_make_parser(subparsers): parser.add_argument( "testcase_path", nargs="*", help="Specify YAML/JSON testcase file/folder path" ) - parser.add_argument( - "--chain-style", - dest="chain_style", - action="store_true", - help="Convert pytest files in chain-style.", - ) return parser diff --git a/httprunner/runner.py b/httprunner/runner.py index b2c9060f..b42aef3a 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -2,7 +2,7 @@ import os import time import uuid from datetime import datetime -from typing import List, Dict, Text, NoReturn, Union +from typing import List, Dict, Text, NoReturn try: import allure @@ -35,8 +35,8 @@ from httprunner.schema import ( class HttpRunner(object): - config: Union[TConfig, Config] - teststeps: List[Union[TStep, Step]] + config: Config + teststeps: List[Step] success: bool = True # indicate testcase execution result __config: TConfig @@ -53,21 +53,10 @@ class HttpRunner(object): __log_path: Text = "" def __init_tests__(self) -> NoReturn: - if isinstance(self.config, TConfig): - self.__config = self.config - elif isinstance(self.config, Config): - self.__config = self.config.perform() - else: - raise exceptions.TestCaseFormatError(f"config type error: {self.config}") - + self.__config = self.config.perform() self.__teststeps = [] for step in self.teststeps: - if isinstance(step, TStep): - self.__teststeps.append(step) - elif isinstance(step, Step): - self.__teststeps.append(step.perform()) - else: - raise exceptions.TestCaseFormatError(f"step type error: {step}") + self.__teststeps.append(step.perform()) def with_project_meta(self, project_meta: ProjectMeta) -> "HttpRunner": self.__project_meta = project_meta diff --git a/httprunner/scaffold.py b/httprunner/scaffold.py index 9fe48471..bf48893f 100644 --- a/httprunner/scaffold.py +++ b/httprunner/scaffold.py @@ -66,7 +66,7 @@ teststeps: validate: - eq: ["status_code", 200] - eq: ["body.args.foo1", "session_bar1"] - - eq: ["body.args.sum_v", 3] + - eq: ["body.args.sum_v", "3"] - eq: ["body.args.foo2", "session_bar2"] - name: post raw text diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 1f772a2a..175e331b 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -189,6 +189,7 @@ class RunTestCase(object): def call(self, testcase: Callable): self.__t_step.testcase = testcase + return self def perform(self) -> TStep: return self.__t_step diff --git a/tests/make_test.py b/tests/make_test.py index 703f5f10..5bb5fbb1 100644 --- a/tests/make_test.py +++ b/tests/make_test.py @@ -43,7 +43,7 @@ from examples.postman_echo.request_methods.request_with_functions_test import ( content, ) self.assertIn( - '"testcase": RequestWithFunctions,', content, + '.call(RequestWithFunctions)', content, ) def test_make_testcase_folder(self): From 7476dc1343cdfbb0d4be125efb129205e55de28c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 18:00:46 +0800 Subject: [PATCH 20/32] feat: add builtin comparators for StepValidation --- httprunner/builtin/comparators.py | 56 +++++++--------- httprunner/response.py | 32 ++++----- httprunner/testcase.py | 106 ++++++++++++++++++++++++++++-- tests/make_test.py | 2 +- tests/utils_test.py | 27 ++++---- 5 files changed, 151 insertions(+), 72 deletions(-) diff --git a/httprunner/builtin/comparators.py b/httprunner/builtin/comparators.py index 8a0f1485..2a8ab78c 100644 --- a/httprunner/builtin/comparators.py +++ b/httprunner/builtin/comparators.py @@ -9,23 +9,23 @@ def equal(check_value, expect_value): assert check_value == expect_value -def less_than(check_value, expect_value): - assert check_value < expect_value - - -def less_than_or_equals(check_value, expect_value): - assert check_value <= expect_value - - def greater_than(check_value, expect_value): assert check_value > expect_value -def greater_than_or_equals(check_value, expect_value): +def less_than(check_value, expect_value): + assert check_value < expect_value + + +def greater_or_equals(check_value, expect_value): assert check_value >= expect_value -def not_equals(check_value, expect_value): +def less_or_equals(check_value, expect_value): + assert check_value <= expect_value + + +def not_equal(check_value, expect_value): assert check_value != expect_value @@ -33,34 +33,29 @@ def string_equals(check_value, expect_value): assert str(check_value) == str(expect_value) -def length_equals(check_value, expect_value): +def length_equal(check_value, expect_value): assert isinstance(expect_value, int) - expect_len = _cast_to_int(expect_value) - assert len(check_value) == expect_len + assert len(check_value) == expect_value def length_greater_than(check_value, expect_value): - assert isinstance(expect_value, int) - expect_len = _cast_to_int(expect_value) - assert len(check_value) > expect_len + assert isinstance(expect_value, (int, float)) + assert len(check_value) > expect_value -def length_greater_than_or_equals(check_value, expect_value): - assert isinstance(expect_value, int) - expect_len = _cast_to_int(expect_value) - assert len(check_value) >= expect_len +def length_greater_or_equals(check_value, expect_value): + assert isinstance(expect_value, (int, float)) + assert len(check_value) >= expect_value def length_less_than(check_value, expect_value): - assert isinstance(expect_value, int) - expect_len = _cast_to_int(expect_value) - assert len(check_value) < expect_len + assert isinstance(expect_value, (int, float)) + assert len(check_value) < expect_value -def length_less_than_or_equals(check_value, expect_value): - assert isinstance(expect_value, int) - expect_len = _cast_to_int(expect_value) - assert len(check_value) <= expect_len +def length_less_or_equals(check_value, expect_value): + assert isinstance(expect_value, (int, float)) + assert len(check_value) <= expect_value def contains(check_value, expect_value): @@ -100,10 +95,3 @@ def startswith(check_value, expect_value): def endswith(check_value, expect_value): assert str(check_value).endswith(str(expect_value)) - - -def _cast_to_int(expect_value): - try: - return int(expect_value) - except Exception: - raise AssertionError("%r can't cast to int" % str(expect_value)) diff --git a/httprunner/response.py b/httprunner/response.py index 7733b90c..9a62c708 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -16,41 +16,35 @@ def get_uniform_comparator(comparator: Text): return "equal" elif comparator in ["lt", "less_than"]: return "less_than" - elif comparator in ["le", "less_than_or_equals"]: - return "less_than_or_equals" + elif comparator in ["le", "less_or_equals"]: + return "less_or_equals" elif comparator in ["gt", "greater_than"]: return "greater_than" - elif comparator in ["ge", "greater_than_or_equals"]: - return "greater_than_or_equals" - elif comparator in ["ne", "not_equals"]: - return "not_equals" + elif comparator in ["ge", "greater_or_equals"]: + return "greater_or_equals" + elif comparator in ["ne", "not_equal"]: + return "not_equal" elif comparator in ["str_eq", "string_equals"]: return "string_equals" - elif comparator in ["len_eq", "length_equals", "count_eq"]: - return "length_equals" + elif comparator in ["len_eq", "length_equal"]: + return "length_equal" elif comparator in [ "len_gt", - "count_gt", "length_greater_than", - "count_greater_than", ]: return "length_greater_than" elif comparator in [ "len_ge", - "count_ge", - "length_greater_than_or_equals", - "count_greater_than_or_equals", + "length_greater_or_equals", ]: - return "length_greater_than_or_equals" - elif comparator in ["len_lt", "count_lt", "length_less_than", "count_less_than"]: + return "length_greater_or_equals" + elif comparator in ["len_lt", "length_less_than"]: return "length_less_than" elif comparator in [ "len_le", - "count_le", - "length_less_than_or_equals", - "count_less_than_or_equals", + "length_less_or_equals", ]: - return "length_less_than_or_equals" + return "length_less_or_equals" else: return comparator diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 175e331b..3828c2a8 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -55,19 +55,117 @@ class StepValidation(object): self.__t_step = step def assert_equal(self, jmes_path: Text, expected_value: Any) -> "StepValidation": - self.__t_step.validators.append({"eq": [jmes_path, expected_value]}) + self.__t_step.validators.append({"equal": [jmes_path, expected_value]}) + return self + + def assert_not_equal( + self, jmes_path: Text, expected_value: Any + ) -> "StepValidation": + self.__t_step.validators.append({"not_equal": [jmes_path, expected_value]}) return self def assert_greater_than( - self, jmes_path: Text, expected_value: Any + self, jmes_path: Text, expected_value: Union[int, float] ) -> "StepValidation": - self.__t_step.validators.append({"gt": [jmes_path, expected_value]}) + self.__t_step.validators.append({"greater_than": [jmes_path, expected_value]}) return self def assert_less_than( + self, jmes_path: Text, expected_value: Union[int, float] + ) -> "StepValidation": + self.__t_step.validators.append({"less_than": [jmes_path, expected_value]}) + return self + + def assert_greater_or_equals( + self, jmes_path: Text, expected_value: Union[int, float] + ) -> "StepValidation": + self.__t_step.validators.append( + {"greater_or_equals": [jmes_path, expected_value]} + ) + return self + + def assert_less_or_equals( + self, jmes_path: Text, expected_value: Union[int, float] + ) -> "StepValidation": + self.__t_step.validators.append({"less_or_equals": [jmes_path, expected_value]}) + return self + + def assert_length_equal( + self, jmes_path: Text, expected_value: int + ) -> "StepValidation": + self.__t_step.validators.append({"length_equal": [jmes_path, expected_value]}) + return self + + def assert_length_greater_than( + self, jmes_path: Text, expected_value: int + ) -> "StepValidation": + self.__t_step.validators.append( + {"length_greater_than": [jmes_path, expected_value]} + ) + return self + + def assert_length_less_than( + self, jmes_path: Text, expected_value: int + ) -> "StepValidation": + self.__t_step.validators.append( + {"length_less_than": [jmes_path, expected_value]} + ) + return self + + def assert_length_greater_or_equals( + self, jmes_path: Text, expected_value: int + ) -> "StepValidation": + self.__t_step.validators.append( + {"length_greater_or_equals": [jmes_path, expected_value]} + ) + return self + + def assert_length_less_or_equals( + self, jmes_path: Text, expected_value: int + ) -> "StepValidation": + self.__t_step.validators.append( + {"length_less_or_equals": [jmes_path, expected_value]} + ) + return self + + def assert_string_equals( + self, jmes_path: Text, expected_value: int + ) -> "StepValidation": + self.__t_step.validators.append({"string_equals": [jmes_path, expected_value]}) + return self + + def assert_startswith( + self, jmes_path: Text, expected_value: Text + ) -> "StepValidation": + self.__t_step.validators.append({"startswith": [jmes_path, expected_value]}) + return self + + def assert_endswith( + self, jmes_path: Text, expected_value: Text + ) -> "StepValidation": + self.__t_step.validators.append({"endswith": [jmes_path, expected_value]}) + return self + + def assert_regex_match( + self, jmes_path: Text, expected_value: Text + ) -> "StepValidation": + self.__t_step.validators.append({"regex_match": [jmes_path, expected_value]}) + return self + + def assert_contains(self, jmes_path: Text, expected_value: Any) -> "StepValidation": + self.__t_step.validators.append({"contains": [jmes_path, expected_value]}) + return self + + def assert_contained_by( self, jmes_path: Text, expected_value: Any ) -> "StepValidation": - self.__t_step.validators.append({"lt": [jmes_path, expected_value]}) + self.__t_step.validators.append({"contained_by": [jmes_path, expected_value]}) + return self + + def assert_type_match( + self, jmes_path: Text, expected_value: Text + ) -> "StepValidation": + self.__t_step.validators.append({"type_match": [jmes_path, expected_value]}) return self def perform(self) -> TStep: diff --git a/tests/make_test.py b/tests/make_test.py index 5bb5fbb1..2189c1f7 100644 --- a/tests/make_test.py +++ b/tests/make_test.py @@ -43,7 +43,7 @@ from examples.postman_echo.request_methods.request_with_functions_test import ( content, ) self.assertIn( - '.call(RequestWithFunctions)', content, + ".call(RequestWithFunctions)", content, ) def test_make_testcase_folder(self): diff --git a/tests/utils_test.py b/tests/utils_test.py index 6558325c..cfb86e85 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -12,33 +12,32 @@ class TestUtils(unittest.TestCase): self.assertIn("abc", os.environ) self.assertEqual(os.environ["abc"], "123") - def current_validators(self): + def test_validators(self): from httprunner.builtin import comparators functions_mapping = loader.load_module_functions(comparators) - functions_mapping["equals"](None, None) - functions_mapping["equals"](1, 1) - functions_mapping["equals"]("abc", "abc") + functions_mapping["equal"](None, None) + functions_mapping["equal"](1, 1) + functions_mapping["equal"]("abc", "abc") with self.assertRaises(AssertionError): - functions_mapping["equals"]("123", 123) + functions_mapping["equal"]("123", 123) functions_mapping["less_than"](1, 2) - functions_mapping["less_than_or_equals"](2, 2) + functions_mapping["less_or_equals"](2, 2) functions_mapping["greater_than"](2, 1) - functions_mapping["greater_than_or_equals"](2, 2) + functions_mapping["greater_or_equals"](2, 2) - functions_mapping["not_equals"](123, "123") + functions_mapping["not_equal"](123, "123") - functions_mapping["length_equals"]("123", 3) - # Because the Numbers in a CSV file are by default treated as strings, - # you need to convert them to Numbers, and we'll test that out here. - functions_mapping["length_equals"]("123", "3") + functions_mapping["length_equal"]("123", 3) with self.assertRaises(AssertionError): - functions_mapping["length_equals"]("123", "abc") + functions_mapping["length_equal"]("123", "3") + with self.assertRaises(AssertionError): + functions_mapping["length_equal"]("123", "abc") functions_mapping["length_greater_than"]("123", 2) - functions_mapping["length_greater_than_or_equals"]("123", 3) + functions_mapping["length_greater_or_equals"]("123", 3) functions_mapping["contains"]("123abc456", "3ab") functions_mapping["contains"](["1", "2"], "1") From cb0984aadb92d99056f65fb6d9e25883c29c890d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 18:32:31 +0800 Subject: [PATCH 21/32] refactor: builtin upload file method --- examples/httpbin/upload_test.py | 61 ++++++++++++--------------------- httprunner/loader.py | 1 - httprunner/make.py | 4 +++ httprunner/testcase.py | 4 +++ 4 files changed, 29 insertions(+), 41 deletions(-) diff --git a/examples/httpbin/upload_test.py b/examples/httpbin/upload_test.py index 986b3746..bb67ec5e 100644 --- a/examples/httpbin/upload_test.py +++ b/examples/httpbin/upload_test.py @@ -1,54 +1,35 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/httpbin/upload.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseUpload(HttpRunner): - config = TConfig( - **{ - "name": "test upload file with httpbin", - "base_url": "${get_httpbin_server()}", - "path": "examples/httpbin/upload_test.py", - "variables": {}, - } - ) + config = Config("test upload file with httpbin").base_url("${get_httpbin_server()}") teststeps = [ - TStep( - **{ - "name": "upload file", - "variables": { + Step( + RunRequest("upload file") + .with_variables( + **{ "file_path": "test.env", "m_encoder": "${multipart_encoder(file=$file_path)}", - }, - "request": { - "url": "/post", - "method": "POST", - "headers": { - "Content-Type": "${multipart_content_type($m_encoder)}" - }, - "data": "$m_encoder", - }, - "validate": [ - {"eq": ["status_code", 200]}, - {"startswith": ["body.files.file", "UserName=test"]}, - ], - } + } + ) + .post("/post") + .with_headers(**{"Content-Type": "${multipart_content_type($m_encoder)}"}) + .with_data("$m_encoder") + .validate() + .assert_equal("status_code", 200) + .assert_startswith("body.files.file", "UserName=test") ), - TStep( - **{ - "name": "upload file with keyword", - "request": { - "url": "/post", - "method": "POST", - "upload": {"file": "test.env"}, - }, - "validate": [ - {"eq": ["status_code", 200]}, - {"startswith": ["body.files.file", "UserName=test"]}, - ], - } + Step( + RunRequest("upload file with keyword") + .post("/post") + .upload(**{"file": "test.env"}) + .validate() + .assert_equal("status_code", 200) + .assert_startswith("body.files.file", "UserName=test") ), ] diff --git a/httprunner/loader.py b/httprunner/loader.py index 27ea6124..e34d4993 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -48,7 +48,6 @@ def _load_json_file(json_file: Text) -> Dict: json_content = json.load(data_file) except json.JSONDecodeError as ex: err_msg = f"JSONDecodeError:\nfile: {json_file}\nerror: {ex}" - logger.error(err_msg) raise exceptions.FileFormatError(err_msg) return json_content diff --git a/httprunner/make.py b/httprunner/make.py index 94dfa6aa..c90956e9 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -184,6 +184,10 @@ def make_request_chain_style(request: Dict) -> Text: allow_redirects = request["allow_redirects"] request_chain_style += f".set_allow_redirects({allow_redirects})" + if "upload" in request: + upload = request["upload"] + request_chain_style += f".upload(**{upload})" + return request_chain_style diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 3828c2a8..bcbfb07d 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -227,6 +227,10 @@ class RequestWithOptionalArgs(object): self.__t_step.request.allow_redirects = allow_redirects return self + def upload(self, **file_info) -> "RequestWithOptionalArgs": + self.__t_step.request.upload.update(file_info) + return self + # def hooks(self): # pass From 1f75ad946dc832de7a63fcf6979c460ce3f320a1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 18:44:05 +0800 Subject: [PATCH 22/32] fix: skip invalid testcase/testsuite yaml/json file --- httprunner/make.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/httprunner/make.py b/httprunner/make.py index c90956e9..157fae4a 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -413,9 +413,7 @@ def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn: # invalid format else: - raise exceptions.FileFormatError( - f"test file is neither testcase nor testsuite: {test_file}" - ) + logger.warning(f"skip invalid testcase/testsuite file: {test_file}") def main_make(tests_paths: List[Text]) -> List[Text]: From 8f78e41f078f2ea4df0d36faf38f9705d2556333 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 18:48:56 +0800 Subject: [PATCH 23/32] docs: update changelog and example tests --- docs/CHANGELOG.md | 13 ++- examples/httpbin/__init__.py | 1 + examples/httpbin/basic_test.py | 143 +++++++++++----------------- examples/httpbin/hooks_test.py | 51 +++------- examples/httpbin/load_image_test.py | 55 +++++------ examples/httpbin/validate.yml | 4 +- examples/httpbin/validate_test.py | 47 ++++----- examples/postman_echo/conftest.py | 62 ------------ 8 files changed, 125 insertions(+), 251 deletions(-) create mode 100644 examples/httpbin/__init__.py delete mode 100644 examples/postman_echo/conftest.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5bb94260..0ba4a75d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,12 +1,23 @@ # Release History -## 3.0.7 (2020-06-01) +## 3.0.7 (2020-06-03) + +**Added** + +- feat: make pytest files in chain style **Fixed** - fix: convert jmespath.search result to int/float unintentionally - fix: referenced testcase should not be run duplicately - fix: requests.cookies.CookieConflictError, multiple cookies with name +- fix: missing exit code from pytest +- fix: skip invalid testcase/testsuite yaml/json file + +**Changed** + +- change: generate pytest in chain style by default +- docs: update sponsor info ## 3.0.6 (2020-05-29) diff --git a/examples/httpbin/__init__.py b/examples/httpbin/__init__.py new file mode 100644 index 00000000..70cfba53 --- /dev/null +++ b/examples/httpbin/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! diff --git a/examples/httpbin/basic_test.py b/examples/httpbin/basic_test.py index 91ffac52..6f03ae4a 100644 --- a/examples/httpbin/basic_test.py +++ b/examples/httpbin/basic_test.py @@ -1,105 +1,76 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/httpbin/basic.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseBasic(HttpRunner): - config = TConfig( - **{ - "name": "basic test with httpbin", - "base_url": "https://httpbin.org/", - "path": "examples/httpbin/basic_test.py", - "variables": {}, - } - ) + config = Config("basic test with httpbin").base_url("https://httpbin.org/") teststeps = [ - TStep( - **{ - "name": "headers", - "request": {"url": "/headers", "method": "GET"}, - "validate": [ - {"eq": ["status_code", 200]}, - {"eq": ["body.headers.Host", "httpbin.org"]}, - ], - } + Step( + RunRequest("headers") + .get("/headers") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.headers.Host", "httpbin.org") ), - TStep( - **{ - "name": "user-agent", - "request": {"url": "/user-agent", "method": "GET"}, - "validate": [ - {"eq": ["status_code", 200]}, - {"startswith": ['body."user-agent"', "python-requests"]}, - ], - } + Step( + RunRequest("user-agent") + .get("/user-agent") + .validate() + .assert_equal("status_code", 200) + .assert_startswith('body."user-agent"', "python-requests") ), - TStep( - **{ - "name": "get without params", - "request": {"url": "/get", "method": "GET"}, - "validate": [{"eq": ["status_code", 200]}, {"eq": ["body.args", {}]}], - } + Step( + RunRequest("get without params") + .get("/get") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args", {}) ), - TStep( - **{ - "name": "get with params in url", - "request": {"url": "/get?a=1&b=2", "method": "GET"}, - "validate": [ - {"eq": ["status_code", 200]}, - {"eq": ["body.args", {"a": "1", "b": "2"}]}, - ], - } + Step( + RunRequest("get with params in url") + .get("/get?a=1&b=2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args", {"a": "1", "b": "2"}) ), - TStep( - **{ - "name": "get with params in params field", - "request": {"url": "/get", "params": {"a": 1, "b": 2}, "method": "GET"}, - "validate": [ - {"eq": ["status_code", 200]}, - {"eq": ["body.args", {"a": "1", "b": "2"}]}, - ], - } + Step( + RunRequest("get with params in params field") + .get("/get") + .with_params(**{"a": 1, "b": 2}) + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args", {"a": "1", "b": "2"}) ), - TStep( - **{ - "name": "set cookie", - "request": {"url": "/cookies/set?name=value", "method": "GET"}, - "validate": [ - {"eq": ["status_code", 200]}, - {"eq": ["body.cookies.name", "value"]}, - ], - } + Step( + RunRequest("set cookie") + .get("/cookies/set?name=value") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.cookies.name", "value") ), - TStep( - **{ - "name": "extract cookie", - "request": {"url": "/cookies", "method": "GET"}, - "validate": [ - {"eq": ["status_code", 200]}, - {"eq": ["body.cookies.name", "value"]}, - ], - } + Step( + RunRequest("extract cookie") + .get("/cookies") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.cookies.name", "value") ), - TStep( - **{ - "name": "post data", - "request": { - "url": "/post", - "method": "POST", - "headers": {"Content-Type": "application/json"}, - "data": "abc", - }, - "validate": [{"eq": ["status_code", 200]}], - } + Step( + RunRequest("post data") + .post("/post") + .with_headers(**{"Content-Type": "application/json"}) + .with_data("abc") + .validate() + .assert_equal("status_code", 200) ), - TStep( - **{ - "name": "validate body length", - "request": {"url": "/spec.json", "method": "GET"}, - "validate": [{"len_eq": ["body", 9]}], - } + Step( + RunRequest("validate body length") + .get("/spec.json") + .validate() + .assert_length_equal("body", 9) ), ] diff --git a/examples/httpbin/hooks_test.py b/examples/httpbin/hooks_test.py index 0e86ee6b..f0f22952 100644 --- a/examples/httpbin/hooks_test.py +++ b/examples/httpbin/hooks_test.py @@ -1,48 +1,27 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/httpbin/hooks.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseHooks(HttpRunner): - config = TConfig( - **{ - "name": "basic test with httpbin", - "base_url": "${get_httpbin_server()}", - "setup_hooks": ["${hook_print(setup)}"], - "teardown_hooks": ["${hook_print(teardown)}"], - "path": "examples/httpbin/hooks_test.py", - "variables": {}, - } - ) + config = Config("basic test with httpbin").base_url("${get_httpbin_server()}") teststeps = [ - TStep( - **{ - "name": "headers", - "variables": {"a": 123}, - "request": {"url": "/headers", "method": "GET"}, - "setup_hooks": [ - "${setup_hook_add_kwargs($request)}", - "${setup_hook_remove_kwargs($request)}", - ], - "teardown_hooks": ["${teardown_hook_sleep_N_secs($response, 1)}"], - "validate": [ - {"eq": ["status_code", 200]}, - {"contained_by": ["body.headers.Host", "${get_httpbin_server()}"]}, - ], - } + Step( + RunRequest("headers") + .with_variables(**{"a": 123}) + .get("/headers") + .validate() + .assert_equal("status_code", 200) + .assert_contained_by("body.headers.Host", "${get_httpbin_server()}") ), - TStep( - **{ - "name": "alter response", - "request": {"url": "/headers", "method": "GET"}, - "teardown_hooks": ["${alter_response($response)}"], - "validate": [ - {"eq": ["status_code", 200]}, - {"eq": ["body.headers.Host", "httpbin.org"]}, - ], - } + Step( + RunRequest("alter response") + .get("/headers") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.headers.Host", "httpbin.org") ), ] diff --git a/examples/httpbin/load_image_test.py b/examples/httpbin/load_image_test.py index 2e69653d..1c276343 100644 --- a/examples/httpbin/load_image_test.py +++ b/examples/httpbin/load_image_test.py @@ -1,47 +1,36 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/httpbin/load_image.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseLoadImage(HttpRunner): - config = TConfig( - **{ - "name": "load images", - "base_url": "${get_httpbin_server()}", - "path": "examples/httpbin/load_image_test.py", - "variables": {}, - } - ) + config = Config("load images").base_url("${get_httpbin_server()}") teststeps = [ - TStep( - **{ - "name": "get png image", - "request": {"url": "/image/png", "method": "GET"}, - "validate": [{"eq": ["status_code", 200]}], - } + Step( + RunRequest("get png image") + .get("/image/png") + .validate() + .assert_equal("status_code", 200) ), - TStep( - **{ - "name": "get jpeg image", - "request": {"url": "/image/jpeg", "method": "GET"}, - "validate": [{"eq": ["status_code", 200]}], - } + Step( + RunRequest("get jpeg image") + .get("/image/jpeg") + .validate() + .assert_equal("status_code", 200) ), - TStep( - **{ - "name": "get webp image", - "request": {"url": "/image/webp", "method": "GET"}, - "validate": [{"eq": ["status_code", 200]}], - } + Step( + RunRequest("get webp image") + .get("/image/webp") + .validate() + .assert_equal("status_code", 200) ), - TStep( - **{ - "name": "get svg image", - "request": {"url": "/image/svg", "method": "GET"}, - "validate": [{"eq": ["status_code", 200]}], - } + Step( + RunRequest("get svg image") + .get("/image/svg") + .validate() + .assert_equal("status_code", 200) ), ] diff --git a/examples/httpbin/validate.yml b/examples/httpbin/validate.yml index c45e2ffd..d5769a7b 100644 --- a/examples/httpbin/validate.yml +++ b/examples/httpbin/validate.yml @@ -13,8 +13,8 @@ teststeps: method: GET validate: - eq: ["status_code", 200] - - eq: ["body.args.a", 1] - - eq: ["body.args.b", 2] + - eq: ["body.args.a", "1"] + - eq: ["body.args.b", "2"] validate_script: - "assert status_code == 200" diff --git a/examples/httpbin/validate_test.py b/examples/httpbin/validate_test.py index fdf2c8a4..24a48178 100644 --- a/examples/httpbin/validate_test.py +++ b/examples/httpbin/validate_test.py @@ -1,43 +1,28 @@ # NOTICE: Generated By HttpRunner. DO NOT EDIT! # FROM: examples/httpbin/validate.yml -from httprunner import HttpRunner, TConfig, TStep +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase class TestCaseValidate(HttpRunner): - config = TConfig( - **{ - "name": "basic test with httpbin", - "base_url": "http://httpbin.org/", - "path": "examples/httpbin/validate_test.py", - "variables": {}, - } - ) + config = Config("basic test with httpbin").base_url("http://httpbin.org/") teststeps = [ - TStep( - **{ - "name": "validate response with json path", - "request": {"url": "/get", "params": {"a": 1, "b": 2}, "method": "GET"}, - "validate": [ - {"eq": ["status_code", 200]}, - {"eq": ["body.args.a", 1]}, - {"eq": ["body.args.b", 2]}, - ], - "validate_script": ["assert status_code == 200"], - } + Step( + RunRequest("validate response with json path") + .get("/get") + .with_params(**{"a": 1, "b": 2}) + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.a", "1") + .assert_equal("body.args.b", "2") ), - TStep( - **{ - "name": "validate response with python script", - "request": {"url": "/get", "params": {"a": 1, "b": 2}, "method": "GET"}, - "validate": [{"eq": ["status_code", 200]}], - "validate_script": [ - "assert status_code == 201", - "a = response_json.get('args').get('a')", - "assert a == '1'", - ], - } + Step( + RunRequest("validate response with python script") + .get("/get") + .with_params(**{"a": 1, "b": 2}) + .validate() + .assert_equal("status_code", 200) ), ] diff --git a/examples/postman_echo/conftest.py b/examples/postman_echo/conftest.py deleted file mode 100644 index 9bb834e6..00000000 --- a/examples/postman_echo/conftest.py +++ /dev/null @@ -1,62 +0,0 @@ -# NOTICE: Generated By HttpRunner. -import json -import os -import time - -import pytest -from loguru import logger - -from httprunner.utils import get_platform - - -@pytest.fixture(scope="session", autouse=True) -def session_fixture(request): - """setup and teardown each task""" - logger.info(f"start running testcases ...") - - start_at = time.time() - - yield - - logger.info(f"task finished, generate task summary for --save-tests") - - summary = { - "success": True, - "stat": { - "testcases": {"total": 0, "success": 0, "fail": 0}, - "teststeps": {"total": 0, "failures": 0, "successes": 0}, - }, - "time": {"start_at": start_at, "duration": time.time() - start_at}, - "platform": get_platform(), - "details": [], - } - - for item in request.node.items: - testcase_summary = item.instance.get_summary() - summary["success"] &= testcase_summary.success - - summary["stat"]["testcases"]["total"] += 1 - summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_datas) - if testcase_summary.success: - summary["stat"]["testcases"]["success"] += 1 - summary["stat"]["teststeps"]["successes"] += len( - testcase_summary.step_datas - ) - else: - summary["stat"]["testcases"]["fail"] += 1 - summary["stat"]["teststeps"]["successes"] += ( - len(testcase_summary.step_datas) - 1 - ) - summary["stat"]["teststeps"]["failures"] += 1 - - summary["details"].append(testcase_summary.dict()) - - summary_path = "/Users/debugtalk/MyProjects/HttpRunner-dev/HttpRunner/examples/postman_echo/logs/request_methods/hardcode.summary.json" - summary_dir = os.path.dirname(summary_path) - os.makedirs(summary_dir, exist_ok=True) - - with open(summary_path, "w", encoding="utf-8") as f: - json.dump(summary, f, indent=4) - - logger.info(f"generated task summary: {summary_path}") - From 7bb06aed725b1590e9772eadf3b4134eeeeddf7d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 19:12:06 +0800 Subject: [PATCH 24/32] change: make private functions public --- httprunner/make.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/httprunner/make.py b/httprunner/make.py index 157fae4a..43c250d5 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -125,7 +125,7 @@ def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]: return testcase_python_path, name_in_title_case -def __format_pytest_with_black(python_paths: List[Text]) -> NoReturn: +def format_pytest_with_black(python_paths: List[Text]) -> NoReturn: logger.info("format pytest cases with black ...") try: subprocess.run(["black", *python_paths]) @@ -236,7 +236,7 @@ def make_teststep_chain_style(teststep: Dict) -> Text: return f"Step({step_info})" -def __make_testcase( +def make_testcase( testcase: Dict, dir_path: Text = None, ref_flag: bool = False, ) -> NoReturn: """convert valid testcase dict to pytest file path""" @@ -318,7 +318,7 @@ def __make_testcase( make_files_cache_set.add(__ensure_cwd_relative(testcase_python_path)) -def __make_testsuite(testsuite: Dict) -> NoReturn: +def make_testsuite(testsuite: Dict) -> NoReturn: """convert valid testsuite dict to pytest folder with testcases""" # validate testsuite format load_testsuite(testsuite) @@ -363,7 +363,7 @@ def __make_testsuite(testsuite: Dict) -> NoReturn: testcase_dict["config"]["variables"].update(testsuite_variables) # make testcase - __make_testcase(testcase_dict, testsuite_dir) + make_testcase(testcase_dict, testsuite_dir) def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn: @@ -400,14 +400,14 @@ def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn: # testcase if "teststeps" in test_content: try: - __make_testcase(test_content, ref_flag=ref_flag) + make_testcase(test_content, ref_flag=ref_flag) except exceptions.TestCaseFormatError: continue # testsuite elif "testcases" in test_content: try: - __make_testsuite(test_content) + make_testsuite(test_content) except exceptions.TestSuiteFormatError: continue @@ -424,7 +424,7 @@ def main_make(tests_paths: List[Text]) -> List[Text]: __make(tests_path) testcase_path_list = list(make_files_cache_set) - __format_pytest_with_black(testcase_path_list) + format_pytest_with_black(testcase_path_list) return testcase_path_list From b47509cdaa3fb98647b8f6aa4dda9d0ff7f27646 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 20:00:47 +0800 Subject: [PATCH 25/32] change: har2case generate pytest file by default --- docs/CHANGELOG.md | 2 +- httprunner/ext/har2case/__init__.py | 17 +++++++++++++++-- httprunner/ext/har2case/core.py | 13 ++++++++++--- httprunner/make.py | 14 ++++++++------ 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0ba4a75d..56b7d55f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,7 +16,7 @@ **Changed** -- change: generate pytest in chain style by default +- change: har2case generate pytest file by default - docs: update sponsor info ## 3.0.6 (2020-05-29) diff --git a/httprunner/ext/har2case/__init__.py b/httprunner/ext/har2case/__init__.py index 3847d7c3..ade2ac36 100644 --- a/httprunner/ext/har2case/__init__.py +++ b/httprunner/ext/har2case/__init__.py @@ -30,7 +30,14 @@ def init_har2case_parser(subparsers): "--to-yaml", dest="to_yaml", action="store_true", - help="Convert to YAML format, if not specified, convert to JSON format by default.", + help="Convert to YAML format, if not specified, convert to pytest format by default.", + ) + parser.add_argument( + "-2j", + "--to-json", + dest="to_json", + action="store_true", + help="Convert to JSON format, if not specified, convert to pytest format by default.", ) parser.add_argument( "--filter", @@ -55,7 +62,13 @@ def main_har2case(args): logger.error(f"HAR file not exists: {har_source_file}") sys.exit(1) - output_file_type = "YML" if args.to_yaml else "JSON" + if args.to_yaml: + output_file_type = "YAML" + elif args.to_yaml: + output_file_type = "JSON" + else: + output_file_type = "pytest" + HarParser(har_source_file, args.filter, args.exclude).gen_testcase(output_file_type) return 0 diff --git a/httprunner/ext/har2case/core.py b/httprunner/ext/har2case/core.py index 3130a91c..56d14464 100644 --- a/httprunner/ext/har2case/core.py +++ b/httprunner/ext/har2case/core.py @@ -7,6 +7,7 @@ import urllib.parse as urlparse from loguru import logger from httprunner.ext.har2case import utils +from httprunner.make import make_testcase, format_pytest_with_black try: from json.decoder import JSONDecodeError @@ -329,17 +330,23 @@ class HarParser(object): testcase = {"config": config, "teststeps": teststeps} return testcase - def gen_testcase(self, file_type="JSON"): + def gen_testcase(self, file_type="pytest"): logger.info(f"Start to generate testcase from {self.har_file_path}") harfile = os.path.splitext(self.har_file_path)[0] - output_testcase_file = "{}.{}".format(harfile, file_type.lower()) testcase = self._make_testcase() logger.debug("prepared testcase: {}".format(testcase)) if file_type == "JSON": + output_testcase_file = f"{harfile}.json" utils.dump_json(testcase, output_testcase_file) - else: + elif file_type == "YAML": + output_testcase_file = f"{harfile}.yml" utils.dump_yaml(testcase, output_testcase_file) + else: + # default to generate pytest file + testcase["config"]["path"] = self.har_file_path + output_testcase_file = make_testcase(testcase) + format_pytest_with_black(output_testcase_file) logger.info(f"generated testcase: {output_testcase_file}") diff --git a/httprunner/make.py b/httprunner/make.py index 43c250d5..a43be11f 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -110,7 +110,7 @@ def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]: raw_file_name, file_suffix = os.path.splitext(os.path.basename(testcase_path)) file_suffix = file_suffix.lower() - if file_suffix not in [".json", ".yml", ".yaml"]: + if file_suffix not in [".json", ".yml", ".yaml", ".har"]: raise exceptions.ParamsError( "testcase file should have .yaml/.yml/.json suffix" ) @@ -125,7 +125,7 @@ def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]: return testcase_python_path, name_in_title_case -def format_pytest_with_black(python_paths: List[Text]) -> NoReturn: +def format_pytest_with_black(*python_paths: Text) -> NoReturn: logger.info("format pytest cases with black ...") try: subprocess.run(["black", *python_paths]) @@ -231,14 +231,14 @@ def make_teststep_chain_style(teststep: Dict) -> Text: expect = validator["expect"] if isinstance(expect, Text): expect = f'"{expect}"' - step_info += f'.assert_{assert_method}({check}, {expect})' + step_info += f".assert_{assert_method}({check}, {expect})" return f"Step({step_info})" def make_testcase( testcase: Dict, dir_path: Text = None, ref_flag: bool = False, -) -> NoReturn: +) -> Text: """convert valid testcase dict to pytest file path""" # ensure compatibility with testcase format v2 testcase = ensure_testcase_v3(testcase) @@ -257,7 +257,7 @@ def make_testcase( global make_files_cache_set if testcase_python_path in make_files_cache_set: - return + return testcase_python_path config = testcase["config"] config["path"] = __ensure_cwd_relative(testcase_python_path) @@ -317,6 +317,8 @@ def make_testcase( if not ref_flag: make_files_cache_set.add(__ensure_cwd_relative(testcase_python_path)) + return testcase_python_path + def make_testsuite(testsuite: Dict) -> NoReturn: """convert valid testsuite dict to pytest folder with testcases""" @@ -424,7 +426,7 @@ def main_make(tests_paths: List[Text]) -> List[Text]: __make(tests_path) testcase_path_list = list(make_files_cache_set) - format_pytest_with_black(testcase_path_list) + format_pytest_with_black(*testcase_path_list) return testcase_path_list From e017b9b3fdb7376ceb1624b5abe276d732f4eed2 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 21:08:45 +0800 Subject: [PATCH 26/32] feat: hrun supports run pytest files --- docs/CHANGELOG.md | 1 + httprunner/loader.py | 4 ++-- httprunner/make.py | 15 ++++++++++++--- tests/ext/har2case/core_test.py | 2 +- tests/make_test.py | 5 ++++- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 56b7d55f..63debb83 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,7 @@ **Added** - feat: make pytest files in chain style +- feat: `hrun` supports run pytest files **Fixed** diff --git a/httprunner/loader.py b/httprunner/loader.py index e34d4993..aee39014 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -194,7 +194,7 @@ def load_csv_file(csv_file: Text) -> List[Dict]: def load_folder_files(folder_path: Text, recursive: bool = True) -> List: - """ load folder path, return all files endswith yml/yaml/json in list. + """ load folder path, return all files endswith .yml/.yaml/.json/_test.py in list. Args: folder_path (str): specified folder path to load @@ -219,7 +219,7 @@ def load_folder_files(folder_path: Text, recursive: bool = True) -> List: filenames_list = [] for filename in filenames: - if not filename.endswith((".yml", ".yaml", ".json")): + if not filename.lower().endswith((".yml", ".yaml", ".json", "_test.py")): continue filenames_list.append(filename) diff --git a/httprunner/make.py b/httprunner/make.py index a43be11f..c656bad9 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -21,6 +21,7 @@ from httprunner.response import uniform_validator """ cache converted pytest files, avoid duplicate making """ make_files_cache_set: Set = set() +pytest_files_set: Set = set() __TEMPLATE__ = jinja2.Template( """# NOTICE: Generated By HttpRunner. DO NOT EDIT! @@ -387,6 +388,10 @@ def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn: raise exceptions.TestcaseNotFound(f"Invalid tests path: {tests_path}") for test_file in test_files: + if test_file.lower().endswith("_test.py"): + pytest_files_set.add(test_file) + continue + try: test_content = load_test_file(test_file) except (exceptions.FileNotFound, exceptions.FileFormatError) as ex: @@ -419,15 +424,19 @@ def __make(tests_path: Text, ref_flag: bool = False) -> NoReturn: def main_make(tests_paths: List[Text]) -> List[Text]: + if not tests_paths: + return [] + for tests_path in tests_paths: if not os.path.isabs(tests_path): tests_path = os.path.join(os.getcwd(), tests_path) __make(tests_path) - testcase_path_list = list(make_files_cache_set) - format_pytest_with_black(*testcase_path_list) - return testcase_path_list + pytest_files_set.update(make_files_cache_set) + pytest_files_list = list(pytest_files_set) + format_pytest_with_black(*pytest_files_list) + return pytest_files_list def init_make_parser(subparsers): diff --git a/tests/ext/har2case/core_test.py b/tests/ext/har2case/core_test.py index 329dde57..21e818ad 100644 --- a/tests/ext/har2case/core_test.py +++ b/tests/ext/har2case/core_test.py @@ -34,7 +34,7 @@ class TestHar(TestHar2CaseUtils): self.assertIn("validate", teststeps[0]) def test_gen_testcase_yaml(self): - yaml_file = os.path.join(os.path.dirname(__file__), "data", "demo.yaml") + yaml_file = os.path.join(os.path.dirname(__file__), "data", "demo.yml") self.har_parser.gen_testcase(file_type="YAML") self.assertTrue(os.path.isfile(yaml_file)) diff --git a/tests/make_test.py b/tests/make_test.py index 2189c1f7..8eec7ca6 100644 --- a/tests/make_test.py +++ b/tests/make_test.py @@ -6,10 +6,11 @@ from httprunner.make import ( make_files_cache_set, make_config_chain_style, make_teststep_chain_style, + pytest_files_set, ) -class TestLoader(unittest.TestCase): +class TestMake(unittest.TestCase): def test_make_testcase(self): path = ["examples/postman_echo/request_methods/request_with_variables.yml"] testcase_python_list = main_make(path) @@ -23,6 +24,7 @@ class TestLoader(unittest.TestCase): "examples/postman_echo/request_methods/request_with_testcase_reference.yml" ] make_files_cache_set.clear() + pytest_files_set.clear() testcase_python_list = main_make(path) self.assertEqual(len(testcase_python_list), 1) self.assertIn( @@ -91,6 +93,7 @@ from examples.postman_echo.request_methods.request_with_functions_test import ( def test_make_testsuite(self): path = ["examples/postman_echo/request_methods/demo_testsuite.yml"] make_files_cache_set.clear() + pytest_files_set.clear() testcase_python_list = main_make(path) self.assertEqual(len(testcase_python_list), 2) self.assertIn( From 82b8975691873409700642504b7278904f71e2e5 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 21:36:05 +0800 Subject: [PATCH 27/32] feat: get raw testcase model from pytest file --- httprunner/make.py | 2 +- httprunner/runner.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/httprunner/make.py b/httprunner/make.py index c656bad9..b5fe6fd6 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -24,7 +24,7 @@ make_files_cache_set: Set = set() pytest_files_set: Set = set() __TEMPLATE__ = jinja2.Template( - """# NOTICE: Generated By HttpRunner. DO NOT EDIT! + """# NOTICE: Generated By HttpRunner. # FROM: {{ testcase_path }} {% if imports_list %} import os diff --git a/httprunner/runner.py b/httprunner/runner.py index b42aef3a..9b94cf5e 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -58,6 +58,13 @@ class HttpRunner(object): for step in self.teststeps: self.__teststeps.append(step.perform()) + @property + def raw_testcase(self) -> TestCase: + if not hasattr(self, "__config"): + self.__init_tests__() + + return TestCase(config=self.__config, teststeps=self.__teststeps) + def with_project_meta(self, project_meta: ProjectMeta) -> "HttpRunner": self.__project_meta = project_meta return self From 94fbab82adeda9e45723044a981bf6a2545abf14 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 21:41:24 +0800 Subject: [PATCH 28/32] fix: missing upload dependencies in tests --- .github/workflows/integration_test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 6064459e..0c234757 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -24,7 +24,7 @@ jobs: python -m pip install --upgrade pip pip install poetry poetry --version - poetry install -vv + poetry install -vv -E upload - name: Test package installation run: | poetry build @@ -36,7 +36,7 @@ jobs: httprunner har2case -h - name: Run smoketest - postman echo run: | - hrun -s examples/postman_echo/request_methods + hrun examples/postman_echo/request_methods - name: Run smoketest - httpbin run: | - hrun -s examples/httpbin/ + hrun examples/httpbin/ From d1b7cdb14c59dfa444f476b3bc0e204f317bdad2 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 21:52:11 +0800 Subject: [PATCH 29/32] fix #914: handle invalid test file --- httprunner/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httprunner/loader.py b/httprunner/loader.py index aee39014..b90e2d5f 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -88,8 +88,8 @@ def load_testcase(testcase: Dict) -> TestCase: 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) + testcase_obj.config.path = testcase_file return testcase_obj From 1f493a1ce44a0d6eeb996be547521bc09f984d05 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 21:55:27 +0800 Subject: [PATCH 30/32] fix #913: handle exception when request json is list type --- httprunner/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httprunner/schema.py b/httprunner/schema.py index 23faf395..59ff17f6 100644 --- a/httprunner/schema.py +++ b/httprunner/schema.py @@ -50,7 +50,7 @@ class TRequest(BaseModel): url: Url params: Dict[Text, Text] = {} headers: Headers = {} - req_json: Dict = Field({}, alias="json") + req_json: Union[Dict, List] = Field({}, alias="json") data: Union[Text, Dict[Text, Any]] = "" cookies: Cookies = {} timeout: float = 120 From 7e1e69fb1c8d9e9acb53a99ef8e94dd72cfb964a Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 22:07:29 +0800 Subject: [PATCH 31/32] fix: load_testcase --- .../demo_testsuite_yml/request_with_functions_test.py | 2 +- .../demo_testsuite_yml/request_with_testcase_reference_test.py | 2 +- examples/postman_echo/request_methods/hardcode_test.py | 2 +- .../request_methods/request_with_functions_test.py | 2 +- .../request_methods/request_with_testcase_reference_test.py | 2 +- .../request_methods/request_with_variables_test.py | 2 +- .../request_methods/validate_with_functions_test.py | 2 +- .../request_methods/validate_with_variables_test.py | 2 +- httprunner/loader.py | 3 +-- 9 files changed, 9 insertions(+), 10 deletions(-) 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 01d3e075..323fb401 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,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/postman_echo/request_methods/request_with_functions.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase 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 dac68fef..01547709 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,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml import os diff --git a/examples/postman_echo/request_methods/hardcode_test.py b/examples/postman_echo/request_methods/hardcode_test.py index 4fd3aa2c..3d30e666 100644 --- a/examples/postman_echo/request_methods/hardcode_test.py +++ b/examples/postman_echo/request_methods/hardcode_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/postman_echo/request_methods/hardcode.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase 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 ff7180aa..279fa9ed 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/postman_echo/request_methods/request_with_functions.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase 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 5d8ae4f2..2fa0b1c3 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,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml import os 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 d7fc5241..4fe8e4d4 100644 --- a/examples/postman_echo/request_methods/request_with_variables_test.py +++ b/examples/postman_echo/request_methods/request_with_variables_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/postman_echo/request_methods/request_with_variables.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase 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 6d39269d..b203c16c 100644 --- a/examples/postman_echo/request_methods/validate_with_functions_test.py +++ b/examples/postman_echo/request_methods/validate_with_functions_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/postman_echo/request_methods/validate_with_functions.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase 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 2ae99691..7a46d80d 100644 --- a/examples/postman_echo/request_methods/validate_with_variables_test.py +++ b/examples/postman_echo/request_methods/validate_with_variables_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/postman_echo/request_methods/validate_with_variables.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/httprunner/loader.py b/httprunner/loader.py index b90e2d5f..4ef030e3 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -73,12 +73,11 @@ def load_test_file(test_file: Text) -> Dict: def load_testcase(testcase: Dict) -> TestCase: - path = testcase["config"]["path"] try: # validate with pydantic TestCase model testcase_obj = TestCase.parse_obj(testcase) except ValidationError as ex: - err_msg = f"TestCase ValidationError:\nfile: {path}\nerror: {ex}" + err_msg = f"TestCase ValidationError:\nerror: {ex}\ncontent: {testcase}" logger.error(err_msg) raise exceptions.TestCaseFormatError(err_msg) From cdbbb9478eb414022d21446e6a40f2b6e88d2128 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 3 Jun 2020 22:23:08 +0800 Subject: [PATCH 32/32] fix: github action --- .github/workflows/integration_test.yml | 4 ++-- docs/CHANGELOG.md | 3 ++- examples/httpbin/basic_test.py | 2 +- examples/httpbin/hooks_test.py | 2 +- examples/httpbin/load_image_test.py | 2 +- examples/httpbin/upload_test.py | 2 +- examples/httpbin/validate_test.py | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 0c234757..4948e7cf 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -36,7 +36,7 @@ jobs: httprunner har2case -h - name: Run smoketest - postman echo run: | - hrun examples/postman_echo/request_methods + poetry run hrun examples/postman_echo/request_methods - name: Run smoketest - httpbin run: | - hrun examples/httpbin/ + poetry run hrun examples/httpbin/ diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 63debb83..5ad8d360 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,7 @@ - feat: make pytest files in chain style - feat: `hrun` supports run pytest files +- feat: get raw testcase model from pytest file **Fixed** @@ -17,7 +18,7 @@ **Changed** -- change: har2case generate pytest file by default +- change: `har2case` generate pytest file by default - docs: update sponsor info ## 3.0.6 (2020-05-29) diff --git a/examples/httpbin/basic_test.py b/examples/httpbin/basic_test.py index 6f03ae4a..37741c07 100644 --- a/examples/httpbin/basic_test.py +++ b/examples/httpbin/basic_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/httpbin/basic.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/hooks_test.py b/examples/httpbin/hooks_test.py index f0f22952..01ebf292 100644 --- a/examples/httpbin/hooks_test.py +++ b/examples/httpbin/hooks_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/httpbin/hooks.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/load_image_test.py b/examples/httpbin/load_image_test.py index 1c276343..9e36a4f8 100644 --- a/examples/httpbin/load_image_test.py +++ b/examples/httpbin/load_image_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/httpbin/load_image.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/upload_test.py b/examples/httpbin/upload_test.py index bb67ec5e..64dfcdbd 100644 --- a/examples/httpbin/upload_test.py +++ b/examples/httpbin/upload_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/httpbin/upload.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/validate_test.py b/examples/httpbin/validate_test.py index 24a48178..c6ae7099 100644 --- a/examples/httpbin/validate_test.py +++ b/examples/httpbin/validate_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. DO NOT EDIT! +# NOTICE: Generated By HttpRunner. # FROM: examples/httpbin/validate.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase