From 8c618d23256b3476288c8f91dbd0cf8e4957e8d9 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 14 Mar 2018 11:57:35 +0800 Subject: [PATCH] add hook feature: 1, setup_hook: call function before request 2, teardown_hook: call function after request --- httprunner/__about__.py | 2 +- httprunner/built_in.py | 50 ++++++++++++++++++++++++++++---- httprunner/client.py | 37 ------------------------ httprunner/events.py | 28 ++++++++++++++++++ httprunner/runner.py | 64 +++++++++++++++++++++++++++++++---------- tests/test_client.py | 8 ++++-- tests/test_context.py | 2 +- tests/test_runner.py | 2 +- 8 files changed, 129 insertions(+), 64 deletions(-) create mode 100644 httprunner/events.py diff --git a/httprunner/__about__.py b/httprunner/__about__.py index 8fb6c40f..81f127f9 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.1.0' +__version__ = '1.2.0-beta' __author__ = 'debugtalk' __author_email__ = 'mail@debugtalk.com' __license__ = 'MIT' diff --git a/httprunner/built_in.py b/httprunner/built_in.py index ecf150c4..32bbb851 100644 --- a/httprunner/built_in.py +++ b/httprunner/built_in.py @@ -1,7 +1,7 @@ """ Built-in dependent functions used in YAML/JSON testcases. """ - +import json import datetime import random import re @@ -31,11 +31,6 @@ def get_current_date(fmt="%Y-%m-%d"): """ return datetime.datetime.now().strftime(fmt) -def sleep(sec): - """ sleep specified seconds - """ - time.sleep(sec) - """ built-in comparators """ @@ -112,3 +107,46 @@ def startswith(check_value, expect_value): def endswith(check_value, expect_value): assert str(check_value).endswith(str(expect_value)) + +""" built-in hooks +""" +def get_charset_from_content_type(content_type): + """ extract charset encoding type from Content-Type + @param content_type + e.g. + application/json; charset=UTF-8 + application/x-www-form-urlencoded; charset=UTF-8 + @return: charset encoding type + UTF-8 + """ + content_type = content_type.lower() + if "charset=" not in content_type: + return None + + index = content_type.index("charset=") + len("charset=") + return content_type[index:] + +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: + # if request content-type is application/json, request data should be dumped + if content_type.startswith("application/json"): + kwargs["data"] = json.dumps(kwargs["data"]) + + # if charset is specified in content-type, request data should be encoded with charset encoding + charset = get_charset_from_content_type(content_type) + if charset: + kwargs["data"] = kwargs["data"].encode(charset) + +def setup_hook_httpntlmauth(method, url, kwargs): + if "httpntlmauth" in kwargs: + from requests_ntlm import HttpNtlmAuth + auth_account = kwargs.pop("httpntlmauth") + kwargs["auth"] = HttpNtlmAuth( + auth_account["username"], auth_account["password"]) + +def teardown_hook_sleep_1_secs(resp_obj): + """ sleep 1 seconds after request + """ + time.sleep(1) diff --git a/httprunner/client.py b/httprunner/client.py index 16dc56d3..5f3efa38 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -1,4 +1,3 @@ -import json import re import time @@ -14,41 +13,6 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) absolute_http_url_regexp = re.compile(r"^https?://", re.I) -def get_charset_from_content_type(content_type): - """ extract charset encoding type from Content-Type - @param content_type - e.g. - application/json; charset=UTF-8 - application/x-www-form-urlencoded; charset=UTF-8 - @return: charset encoding type - UTF-8 - """ - content_type = content_type.lower() - if "charset=" not in content_type: - return None - - index = content_type.index("charset=") + len("charset=") - return content_type[index:] - -def prepare_kwargs(method, kwargs): - if method == "POST": - content_type = kwargs.get("headers", {}).get("content-type") - if content_type and "data" in kwargs: - # if request content-type is application/json, request data should be dumped - if content_type.startswith("application/json"): - kwargs["data"] = json.dumps(kwargs["data"]) - - # if charset is specified in content-type, request data should be encoded with charset encoding - charset = get_charset_from_content_type(content_type) - if charset: - kwargs["data"] = kwargs["data"].encode(charset) - - if "httpntlmauth" in kwargs: - from requests_ntlm import HttpNtlmAuth - auth_account = kwargs.pop("httpntlmauth") - kwargs["auth"] = HttpNtlmAuth( - auth_account["username"], auth_account["password"]) - class ApiResponse(Response): @@ -183,7 +147,6 @@ class HttpSession(requests.Session): Safe mode has been removed from requests 1.x. """ try: - prepare_kwargs(method, kwargs) logger.log_debug("request kwargs(processed): {kwargs}".format(kwargs=kwargs)) return requests.Session.request(self, method, url, **kwargs) except (MissingSchema, InvalidSchema, InvalidURL): diff --git a/httprunner/events.py b/httprunner/events.py new file mode 100644 index 00000000..34259967 --- /dev/null +++ b/httprunner/events.py @@ -0,0 +1,28 @@ +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 = set() + + def __iadd__(self, handler): + self._handlers.add(handler) + return self + + def __isub__(self, handler): + self._handlers.remove(handler) + return self + + def fire(self, **kwargs): + for handler in self._handlers: + handler(**kwargs) + diff --git a/httprunner/runner.py b/httprunner/runner.py index 18b1b960..5ae30e87 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -3,6 +3,7 @@ from unittest.case import SkipTest from httprunner import exception, logger, response, testcase, utils from httprunner.client import HttpSession from httprunner.context import Context +from httprunner.events import EventHook class Runner(object): @@ -92,9 +93,44 @@ class Runner(object): if skip_reason: raise SkipTest(skip_reason) - def setup_teardown(self, actions): - for action in actions: - self.context.eval_content(action) + 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 + """ + 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 run_test(self, testcase_dict): """ run single testcase. @@ -116,10 +152,10 @@ class Runner(object): }, "body": '{"name": "user", "password": "123456"}' }, - "extract": [], # optional - "validate": [], # optional - "setup": [], # optional - "teardown": [] # optional + "extract": [], # optional + "validate": [], # optional + "setup_hooks": [], # optional + "teardown_hooks": [] # optional } @return True or raise exception during test """ @@ -132,21 +168,21 @@ class Runner(object): except KeyError: raise exception.ParamsError("URL or METHOD missed!") - extractors = testcase_dict.get("extract", []) - validators = testcase_dict.get("validate", []) - setup_actions = testcase_dict.get("setup", []) - teardown_actions = testcase_dict.get("teardown", []) - self._handle_skip_feature(testcase_dict) - self.setup_teardown(setup_actions) + extractors = testcase_dict.get("extract", []) + validators = testcase_dict.get("validate", []) + setup_hooks = testcase_dict.get("setup_hooks", []) + teardown_hooks = testcase_dict.get("teardown_hooks", []) + self._call_setup_hooks(setup_hooks, method, url, parsed_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) extracted_variables_mapping = resp_obj.extract_response(extractors) @@ -170,8 +206,6 @@ class Runner(object): logger.log_error(err_resp_msg) raise - finally: - self.setup_teardown(teardown_actions) def extract_output(self, output_variables_list): """ extract output variables diff --git a/tests/test_client.py b/tests/test_client.py index 94cab9a3..ef5a3e88 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,6 +1,8 @@ -from httprunner.client import HttpSession, prepare_kwargs +from httprunner.built_in import setup_hook_prepare_kwargs +from httprunner.client import HttpSession from tests.base import ApiServerUnittest + class TestHttpClient(ApiServerUnittest): def setUp(self): super(TestHttpClient, self).setUp() @@ -46,7 +48,7 @@ class TestHttpClient(ApiServerUnittest): "b": 2 } } - prepare_kwargs("POST", kwargs) + setup_hook_prepare_kwargs("POST", "/path", kwargs) self.assertIn('"a": 1', kwargs["data"]) self.assertIn('"b": 2', kwargs["data"]) @@ -60,5 +62,5 @@ class TestHttpClient(ApiServerUnittest): "b": 2 } } - prepare_kwargs("POST", kwargs) + setup_hook_prepare_kwargs("POST", "/path", kwargs) self.assertIsInstance(kwargs["data"], bytes) diff --git a/tests/test_context.py b/tests/test_context.py index e099b288..2514f705 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -230,7 +230,7 @@ class VariableBindsUnittest(ApiServerUnittest): def test_exec_content_functions(self): test_runner = runner.Runner() - content = "${sleep(1)}" + content = "${teardown_hook_sleep_1_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 16d0dff8..8b1ed7f9 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -65,7 +65,7 @@ class TestRunner(ApiServerUnittest): {"check": "status_code", "expect": 205}, {"check": "content.token", "comparator": "len_eq", "expect": 19} ], - "teardown": ["${sleep(2)}"] + "teardown_hooks": ["teardown_hook_sleep_1_secs"] } with self.assertRaises(exception.ValidationError):