add hook feature:

1, setup_hook: call function before request
2, teardown_hook: call function after request
This commit is contained in:
debugtalk
2018-03-14 11:57:35 +08:00
parent c7a1a17b3c
commit 8c618d2325
8 changed files with 129 additions and 64 deletions

View File

@@ -1,7 +1,7 @@
__title__ = 'HttpRunner' __title__ = 'HttpRunner'
__description__ = 'One-stop solution for HTTP(S) testing.' __description__ = 'One-stop solution for HTTP(S) testing.'
__url__ = 'https://github.com/HttpRunner/HttpRunner' __url__ = 'https://github.com/HttpRunner/HttpRunner'
__version__ = '1.1.0' __version__ = '1.2.0-beta'
__author__ = 'debugtalk' __author__ = 'debugtalk'
__author_email__ = 'mail@debugtalk.com' __author_email__ = 'mail@debugtalk.com'
__license__ = 'MIT' __license__ = 'MIT'

View File

@@ -1,7 +1,7 @@
""" """
Built-in dependent functions used in YAML/JSON testcases. Built-in dependent functions used in YAML/JSON testcases.
""" """
import json
import datetime import datetime
import random import random
import re import re
@@ -31,11 +31,6 @@ def get_current_date(fmt="%Y-%m-%d"):
""" """
return datetime.datetime.now().strftime(fmt) return datetime.datetime.now().strftime(fmt)
def sleep(sec):
""" sleep specified seconds
"""
time.sleep(sec)
""" built-in comparators """ built-in comparators
""" """
@@ -112,3 +107,46 @@ def startswith(check_value, expect_value):
def endswith(check_value, expect_value): def endswith(check_value, expect_value):
assert str(check_value).endswith(str(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)

View File

@@ -1,4 +1,3 @@
import json
import re import re
import time import time
@@ -14,41 +13,6 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
absolute_http_url_regexp = re.compile(r"^https?://", re.I) 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): class ApiResponse(Response):
@@ -183,7 +147,6 @@ class HttpSession(requests.Session):
Safe mode has been removed from requests 1.x. Safe mode has been removed from requests 1.x.
""" """
try: try:
prepare_kwargs(method, kwargs)
logger.log_debug("request kwargs(processed): {kwargs}".format(kwargs=kwargs)) logger.log_debug("request kwargs(processed): {kwargs}".format(kwargs=kwargs))
return requests.Session.request(self, method, url, **kwargs) return requests.Session.request(self, method, url, **kwargs)
except (MissingSchema, InvalidSchema, InvalidURL): except (MissingSchema, InvalidSchema, InvalidURL):

28
httprunner/events.py Normal file
View File

@@ -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)

View File

@@ -3,6 +3,7 @@ from unittest.case import SkipTest
from httprunner import exception, logger, response, testcase, utils from httprunner import exception, logger, response, testcase, utils
from httprunner.client import HttpSession from httprunner.client import HttpSession
from httprunner.context import Context from httprunner.context import Context
from httprunner.events import EventHook
class Runner(object): class Runner(object):
@@ -92,9 +93,44 @@ class Runner(object):
if skip_reason: if skip_reason:
raise SkipTest(skip_reason) raise SkipTest(skip_reason)
def setup_teardown(self, actions): def _prepare_hooks_event(self, hooks):
for action in actions: if not hooks:
self.context.eval_content(action) 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): def run_test(self, testcase_dict):
""" run single testcase. """ run single testcase.
@@ -116,10 +152,10 @@ class Runner(object):
}, },
"body": '{"name": "user", "password": "123456"}' "body": '{"name": "user", "password": "123456"}'
}, },
"extract": [], # optional "extract": [], # optional
"validate": [], # optional "validate": [], # optional
"setup": [], # optional "setup_hooks": [], # optional
"teardown": [] # optional "teardown_hooks": [] # optional
} }
@return True or raise exception during test @return True or raise exception during test
""" """
@@ -132,21 +168,21 @@ class Runner(object):
except KeyError: except KeyError:
raise exception.ParamsError("URL or METHOD missed!") 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._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( resp = self.http_client_session.request(
method, method,
url, url,
name=group_name, name=group_name,
**parsed_request **parsed_request
) )
self._call_teardown_hooks(teardown_hooks, resp)
resp_obj = response.ResponseObject(resp) resp_obj = response.ResponseObject(resp)
extracted_variables_mapping = resp_obj.extract_response(extractors) extracted_variables_mapping = resp_obj.extract_response(extractors)
@@ -170,8 +206,6 @@ class Runner(object):
logger.log_error(err_resp_msg) logger.log_error(err_resp_msg)
raise raise
finally:
self.setup_teardown(teardown_actions)
def extract_output(self, output_variables_list): def extract_output(self, output_variables_list):
""" extract output variables """ extract output variables

View File

@@ -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 from tests.base import ApiServerUnittest
class TestHttpClient(ApiServerUnittest): class TestHttpClient(ApiServerUnittest):
def setUp(self): def setUp(self):
super(TestHttpClient, self).setUp() super(TestHttpClient, self).setUp()
@@ -46,7 +48,7 @@ class TestHttpClient(ApiServerUnittest):
"b": 2 "b": 2
} }
} }
prepare_kwargs("POST", kwargs) setup_hook_prepare_kwargs("POST", "/path", kwargs)
self.assertIn('"a": 1', kwargs["data"]) self.assertIn('"a": 1', kwargs["data"])
self.assertIn('"b": 2', kwargs["data"]) self.assertIn('"b": 2', kwargs["data"])
@@ -60,5 +62,5 @@ class TestHttpClient(ApiServerUnittest):
"b": 2 "b": 2
} }
} }
prepare_kwargs("POST", kwargs) setup_hook_prepare_kwargs("POST", "/path", kwargs)
self.assertIsInstance(kwargs["data"], bytes) self.assertIsInstance(kwargs["data"], bytes)

View File

@@ -230,7 +230,7 @@ class VariableBindsUnittest(ApiServerUnittest):
def test_exec_content_functions(self): def test_exec_content_functions(self):
test_runner = runner.Runner() test_runner = runner.Runner()
content = "${sleep(1)}" content = "${teardown_hook_sleep_1_secs(1)}"
start_time = time.time() start_time = time.time()
test_runner.context.eval_content(content) test_runner.context.eval_content(content)
end_time = time.time() end_time = time.time()

View File

@@ -65,7 +65,7 @@ class TestRunner(ApiServerUnittest):
{"check": "status_code", "expect": 205}, {"check": "status_code", "expect": 205},
{"check": "content.token", "comparator": "len_eq", "expect": 19} {"check": "content.token", "comparator": "len_eq", "expect": 19}
], ],
"teardown": ["${sleep(2)}"] "teardown_hooks": ["teardown_hook_sleep_1_secs"]
} }
with self.assertRaises(exception.ValidationError): with self.assertRaises(exception.ValidationError):