diff --git a/httprunner/__about__.py b/httprunner/__about__.py index bf3be10f..a14324da 100644 --- a/httprunner/__about__.py +++ b/httprunner/__about__.py @@ -1,7 +1,7 @@ __title__ = 'HttpRunner' __description__ = 'One-stop solution for HTTP(S) testing.' __url__ = 'https://github.com/HttpRunner/HttpRunner' -__version__ = '1.4.2' +__version__ = '1.4.3' __author__ = 'debugtalk' __author_email__ = 'mail@debugtalk.com' __license__ = 'MIT' diff --git a/httprunner/built_in.py b/httprunner/built_in.py index d5d69242..207bc671 100644 --- a/httprunner/built_in.py +++ b/httprunner/built_in.py @@ -132,25 +132,25 @@ def endswith(check_value, expect_value): """ built-in hooks """ -def setup_hook_prepare_kwargs(method, url, kwargs): - if method == "POST": - content_type = kwargs.get("headers", {}).get("content-type") - if content_type and "data" in kwargs: +def setup_hook_prepare_kwargs(request): + if request["method"] == "POST": + content_type = request.get("headers", {}).get("content-type") + if content_type and "data" in request: # if request content-type is application/json, request data should be dumped - if content_type.startswith("application/json") and isinstance(kwargs["data"], (dict, list)): - kwargs["data"] = json.dumps(kwargs["data"]) + if content_type.startswith("application/json") and isinstance(request["data"], (dict, list)): + request["data"] = json.dumps(request["data"]) - if isinstance(kwargs["data"], str): - kwargs["data"] = kwargs["data"].encode('utf-8') + if isinstance(request["data"], str): + request["data"] = request["data"].encode('utf-8') -def setup_hook_httpntlmauth(method, url, kwargs): - if "httpntlmauth" in kwargs: +def setup_hook_httpntlmauth(request): + if "httpntlmauth" in request: from requests_ntlm import HttpNtlmAuth - auth_account = kwargs.pop("httpntlmauth") - kwargs["auth"] = HttpNtlmAuth( + auth_account = request.pop("httpntlmauth") + request["auth"] = HttpNtlmAuth( auth_account["username"], auth_account["password"]) -def teardown_hook_sleep_1_secs(resp_obj): - """ sleep 1 seconds after request +def sleep_N_secs(n_secs): + """ sleep n seconds """ - time.sleep(1) + time.sleep(n_secs) diff --git a/httprunner/events.py b/httprunner/events.py deleted file mode 100644 index 39732e76..00000000 --- a/httprunner/events.py +++ /dev/null @@ -1,35 +0,0 @@ -# encoding: utf-8 -from httprunner.exception import MyBaseError - - -class EventHook(object): - """ - Simple event class used to provide hooks for different types of events in HttpRunner. - - Here's how to use the EventHook class:: - - my_event = EventHook() - def on_my_event(a, b, **kw): - print "Event was fired with arguments: %s, %s" % (a, b) - my_event += on_my_event - my_event.fire(a="foo", b="bar") - """ - - def __init__(self): - self._handlers = [] - - def __iadd__(self, handler): - self._handlers.append(handler) - return self - - def __isub__(self, handler): - if handler not in self._handlers: - raise MyBaseError("handler not found: {}".format(handler)) - - index = self._handlers.index(handler) - self._handlers.pop(index) - return self - - def fire(self, **kwargs): - for handler in self._handlers: - handler(**kwargs) diff --git a/httprunner/runner.py b/httprunner/runner.py index 7cd51e3a..962ff66c 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -5,7 +5,6 @@ from unittest.case import SkipTest from httprunner import exception, logger, response, utils from httprunner.client import HttpSession from httprunner.context import Context -from httprunner.events import EventHook class Runner(object): @@ -94,45 +93,10 @@ class Runner(object): if skip_reason: raise SkipTest(skip_reason) - def _prepare_hooks_event(self, hooks): - if not hooks: - return None - - event = EventHook() - for hook in hooks: - func = self.context.testcase_parser.get_bind_function(hook) - event += func - - return event - - def _call_setup_hooks(self, hooks, method, url, kwargs): - """ call hook functions before request - - Listeners should take the following arguments: - - * *method*: request method type, e.g. GET, POST, PUT - * *url*: URL that was called (or override name if it was used in the call to the client) - * *kwargs*: kwargs of request - """ - hooks.insert(0, "setup_hook_prepare_kwargs") - event = self._prepare_hooks_event(hooks) - if not event: - return - - event.fire(method=method, url=url, kwargs=kwargs) - - def _call_teardown_hooks(self, hooks, resp_obj): - """ call hook functions after request - - Listeners should take the following arguments: - - * *resp_obj*: response object - """ - event = self._prepare_hooks_event(hooks) - if not event: - return - - event.fire(resp_obj=resp_obj) + def do_hook_actions(self, actions): + for action in actions: + logger.log_debug("call hook: {}".format(action)) + self.context.eval_content(action) def run_test(self, testcase_dict): """ run single testcase. @@ -161,7 +125,12 @@ class Runner(object): } @return True or raise exception during test """ + # check skip + self._handle_skip_feature(testcase_dict) + + # prepare parsed_request = self.init_config(testcase_dict, level="testcase") + self.context.bind_variables({"request": parsed_request}) try: url = parsed_request.pop('url') @@ -170,28 +139,36 @@ class Runner(object): except KeyError: raise exception.ParamsError("URL or METHOD missed!") - self._handle_skip_feature(testcase_dict) - - extractors = testcase_dict.get("extract", []) or testcase_dict.get("extractors", []) - validators = testcase_dict.get("validate", []) or testcase_dict.get("validators", []) - setup_hooks = testcase_dict.get("setup_hooks", []) - teardown_hooks = testcase_dict.get("teardown_hooks", []) - logger.log_info("{method} {url}".format(method=method, url=url)) logger.log_debug("request kwargs(raw): {kwargs}".format(kwargs=parsed_request)) - self._call_setup_hooks(setup_hooks, method, url, parsed_request) + + # setup hooks + setup_hooks = testcase_dict.get("setup_hooks", []) + setup_hooks.insert(0, "${setup_hook_prepare_kwargs($request)}") + self.do_hook_actions(setup_hooks) + + # request resp = self.http_client_session.request( method, url, name=group_name, **parsed_request ) - self._call_teardown_hooks(teardown_hooks, resp) - resp_obj = response.ResponseObject(resp) + # teardown hooks + teardown_hooks = testcase_dict.get("teardown_hooks", []) + if teardown_hooks: + self.context.bind_variables({"response": resp}) + self.do_hook_actions(teardown_hooks) + + # extract + extractors = testcase_dict.get("extract", []) or testcase_dict.get("extractors", []) + resp_obj = response.ResponseObject(resp) extracted_variables_mapping = resp_obj.extract_response(extractors) self.context.bind_extracted_variables(extracted_variables_mapping) + # validate + validators = testcase_dict.get("validate", []) or testcase_dict.get("validators", []) try: self.context.validate(validators, resp_obj) except (exception.ParamsError, exception.ResponseError, \ diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 76a13875..9b21615e 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -10,7 +10,7 @@ import random import re from httprunner import exception, logger, utils -from httprunner.compat import OrderedDict, numeric_types +from httprunner.compat import OrderedDict, basestring, numeric_types from httprunner.utils import FileUtils variable_regexp = r"\$([\w_]+)" @@ -75,14 +75,15 @@ def parse_function(content): func(a=1, b=2) => {'func_name': 'func', 'args': [], 'kwargs': {'a': 1, 'b': 2}} func(1, 2, a=3, b=4) => {'func_name': 'func', 'args': [1, 2], 'kwargs': {'a':3, 'b':4}} """ + matched = function_regexp_compile.match(content) + if not matched: + raise exception.FunctionNotFound("{} not found!".format(content)) + function_meta = { + "func_name": matched.group(1), "args": [], "kwargs": {} } - matched = function_regexp_compile.match(content) - if not matched: - raise exception.ApiNotFound("{} not found!".format(content)) - function_meta["func_name"] = matched.group(1) args_str = matched.group(2).replace(" ", "") if args_str == "": @@ -597,6 +598,7 @@ def substitute_variables_with_mapping(content, mapping): } } """ + # TODO: refactor type check if isinstance(content, bool): return content @@ -903,17 +905,16 @@ class TestcaseParser(object): return evaluated_data - if isinstance(content, (numeric_types, type)): - return content + if isinstance(content, basestring): - # content is in string format here - content = content.strip() + # content is in string format here + content = content.strip() - # replace functions with evaluated value - # Notice: _eval_content_functions must be called before _eval_content_variables - content = self._eval_content_functions(content) + # replace functions with evaluated value + # Notice: _eval_content_functions must be called before _eval_content_variables + content = self._eval_content_functions(content) - # replace variables with binding value - content = self._eval_content_variables(content) + # replace variables with binding value + content = self._eval_content_variables(content) return content diff --git a/tests/debugtalk.py b/tests/debugtalk.py index f9330b3d..619b8eab 100644 --- a/tests/debugtalk.py +++ b/tests/debugtalk.py @@ -68,8 +68,16 @@ def gen_random_string(str_len): random_string = ''.join(random_char_list) return random_string -def setup_hook_add_kwargs(method, url, kwargs): - kwargs["key"] = "value" +def setup_hook_add_kwargs(request): + request["key"] = "value" -def setup_hook_remove_kwargs(method, url, kwargs): - kwargs.pop("key") +def setup_hook_remove_kwargs(request): + request.pop("key") + +def teardown_hook_sleep_N_secs(response, n_secs): + """ sleep n seconds after request + """ + if response.status_code == 200: + time.sleep(0.1) + else: + time.sleep(n_secs) diff --git a/tests/httpbin/hooks.yml b/tests/httpbin/hooks.yml index 67cd3339..c30df0b7 100644 --- a/tests/httpbin/hooks.yml +++ b/tests/httpbin/hooks.yml @@ -9,8 +9,10 @@ url: /headers method: GET setup_hooks: - - setup_hook_add_kwargs - - setup_hook_remove_kwargs + - ${setup_hook_add_kwargs($request)} + - ${setup_hook_remove_kwargs($request)} + teardown_hooks: + - ${teardown_hook_sleep_N_secs($response, 1)} validate: - eq: ["status_code", 200] - eq: [content.headers.Host, "127.0.0.1:3458"] diff --git a/tests/test_client.py b/tests/test_client.py index be51953d..f756a4b9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -40,7 +40,9 @@ class TestHttpClient(ApiServerUnittest): self.assertEqual(True, resp.json()['success']) def test_prepare_kwargs_content_type_application_json_without_charset(self): - kwargs = { + request = { + "url": "/path", + "method": "POST", "headers": { "content-type": "application/json" }, @@ -49,13 +51,15 @@ class TestHttpClient(ApiServerUnittest): "b": 2 } } - setup_hook_prepare_kwargs("POST", "/path", kwargs) - self.assertIsInstance(kwargs["data"], bytes) - self.assertIn(b'"a": 1', kwargs["data"]) - self.assertIn(b'"b": 2', kwargs["data"]) + setup_hook_prepare_kwargs(request) + self.assertIsInstance(request["data"], bytes) + self.assertIn(b'"a": 1', request["data"]) + self.assertIn(b'"b": 2', request["data"]) def test_prepare_kwargs_content_type_application_json_charset_utf8(self): - kwargs = { + request = { + "url": "/path", + "method": "POST", "headers": { "content-type": "application/json; charset=utf-8" }, @@ -64,5 +68,5 @@ class TestHttpClient(ApiServerUnittest): "b": 2 } } - setup_hook_prepare_kwargs("POST", "/path", kwargs) - self.assertIsInstance(kwargs["data"], bytes) + setup_hook_prepare_kwargs(request) + self.assertIsInstance(request["data"], bytes) diff --git a/tests/test_context.py b/tests/test_context.py index fb652107..d9beb998 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -231,7 +231,7 @@ class VariableBindsUnittest(ApiServerUnittest): def test_exec_content_functions(self): test_runner = runner.Runner() - content = "${teardown_hook_sleep_1_secs(1)}" + content = "${sleep_N_secs(1)}" start_time = time.time() test_runner.context.eval_content(content) end_time = time.time() diff --git a/tests/test_runner.py b/tests/test_runner.py index d8c4f7ac..aa518575 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -60,29 +60,91 @@ class TestRunner(ApiServerUnittest): "sign": "f1219719911caae89ccc301679857ebfda115ca2" } }, - "extract": [ - {"token": "content.token"} - ], "validate": [ {"check": "status_code", "expect": 205}, {"check": "content.token", "comparator": "len_eq", "expect": 19} - ], - "teardown_hooks": ["teardown_hook_sleep_1_secs"] + ] } with self.assertRaises(exception.ValidationError): - start_time = time.time() self.test_runner.run_test(test) - end_time = time.time() - # check if teardown function executed - self.assertGreater(end_time - start_time, 2) def test_run_testset_with_setup_hooks(self): testcase_file_path = os.path.join( os.getcwd(), 'tests/httpbin/hooks.yml') + + start_time = time.time() runner = HttpRunner().run(testcase_file_path) + end_time = time.time() summary = runner.summary self.assertTrue(summary["success"]) + self.assertLess(end_time - start_time, 1) + + def test_run_testset_with_teardown_hooks_success(self): + test = { + "name": "get token", + "request": { + "url": "http://127.0.0.1:5000/api/get-token", + "method": "POST", + "headers": { + "content-type": "application/json", + "user_agent": "iOS/10.3", + "device_sn": "HZfFBh6tU59EdXJ", + "os_platform": "ios", + "app_version": "2.8.6" + }, + "json": { + "sign": "f1219719911caae89ccc301679857ebfda115ca2" + } + }, + "validate": [ + {"check": "status_code", "expect": 200} + ], + "teardown_hooks": ["${teardown_hook_sleep_N_secs($response, 2)}"] + } + config_dict = { + "path": os.path.join(os.getcwd(), __file__) + } + self.test_runner.init_config(config_dict, "testset") + + start_time = time.time() + self.test_runner.run_test(test) + end_time = time.time() + # check if teardown function executed + self.assertLess(end_time - start_time, 0.5) + + def test_run_testset_with_teardown_hooks_fail(self): + test = { + "name": "get token", + "request": { + "url": "http://127.0.0.1:5000/api/get-token2", + "method": "POST", + "headers": { + "content-type": "application/json", + "user_agent": "iOS/10.3", + "device_sn": "HZfFBh6tU59EdXJ", + "os_platform": "ios", + "app_version": "2.8.6" + }, + "json": { + "sign": "f1219719911caae89ccc301679857ebfda115ca2" + } + }, + "validate": [ + {"check": "status_code", "expect": 404} + ], + "teardown_hooks": ["${teardown_hook_sleep_N_secs($response, 2)}"] + } + config_dict = { + "path": os.path.join(os.getcwd(), __file__) + } + self.test_runner.init_config(config_dict, "testset") + + start_time = time.time() + self.test_runner.run_test(test) + end_time = time.time() + # check if teardown function executed + self.assertGreater(end_time - start_time, 2) def test_run_testset_hardcode(self): for testcase_file_path in self.testcase_file_path_list: diff --git a/tests/test_testcase.py b/tests/test_testcase.py index 34ea0a75..b1cf356e 100644 --- a/tests/test_testcase.py +++ b/tests/test_testcase.py @@ -454,6 +454,10 @@ class TestcaseParserUnittest(unittest.TestCase): testcase.parse_function("func(1, 2, a=3, b=4)"), {'func_name': 'func', 'args': [1, 2], 'kwargs': {'a': 3, 'b': 4}} ) + self.assertEqual( + testcase.parse_function("func($request, 123)"), + {'func_name': 'func', 'args': ["$request", 123], 'kwargs': {}} + ) def test_parse_content_with_bindings_variables(self): variables = {