refactor hook mechanism:

1, remove EventHook;
2, setup_hooks: could reference request dict;
3, teardown_hooks: could reference Response object.
This commit is contained in:
debugtalk
2018-05-10 13:40:47 +08:00
parent ce995a798f
commit dcc1e70181
11 changed files with 162 additions and 139 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.4.2' __version__ = '1.4.3'
__author__ = 'debugtalk' __author__ = 'debugtalk'
__author_email__ = 'mail@debugtalk.com' __author_email__ = 'mail@debugtalk.com'
__license__ = 'MIT' __license__ = 'MIT'

View File

@@ -132,25 +132,25 @@ def endswith(check_value, expect_value):
""" built-in hooks """ built-in hooks
""" """
def setup_hook_prepare_kwargs(method, url, kwargs): def setup_hook_prepare_kwargs(request):
if method == "POST": if request["method"] == "POST":
content_type = kwargs.get("headers", {}).get("content-type") content_type = request.get("headers", {}).get("content-type")
if content_type and "data" in kwargs: if content_type and "data" in request:
# if request content-type is application/json, request data should be dumped # if request content-type is application/json, request data should be dumped
if content_type.startswith("application/json") and isinstance(kwargs["data"], (dict, list)): if content_type.startswith("application/json") and isinstance(request["data"], (dict, list)):
kwargs["data"] = json.dumps(kwargs["data"]) request["data"] = json.dumps(request["data"])
if isinstance(kwargs["data"], str): if isinstance(request["data"], str):
kwargs["data"] = kwargs["data"].encode('utf-8') request["data"] = request["data"].encode('utf-8')
def setup_hook_httpntlmauth(method, url, kwargs): def setup_hook_httpntlmauth(request):
if "httpntlmauth" in kwargs: if "httpntlmauth" in request:
from requests_ntlm import HttpNtlmAuth from requests_ntlm import HttpNtlmAuth
auth_account = kwargs.pop("httpntlmauth") auth_account = request.pop("httpntlmauth")
kwargs["auth"] = HttpNtlmAuth( request["auth"] = HttpNtlmAuth(
auth_account["username"], auth_account["password"]) auth_account["username"], auth_account["password"])
def teardown_hook_sleep_1_secs(resp_obj): def sleep_N_secs(n_secs):
""" sleep 1 seconds after request """ sleep n seconds
""" """
time.sleep(1) time.sleep(n_secs)

View File

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

View File

@@ -5,7 +5,6 @@ from unittest.case import SkipTest
from httprunner import exception, logger, response, utils from httprunner import exception, logger, response, 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):
@@ -94,45 +93,10 @@ class Runner(object):
if skip_reason: if skip_reason:
raise SkipTest(skip_reason) raise SkipTest(skip_reason)
def _prepare_hooks_event(self, hooks): def do_hook_actions(self, actions):
if not hooks: for action in actions:
return None logger.log_debug("call hook: {}".format(action))
self.context.eval_content(action)
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 run_test(self, testcase_dict): def run_test(self, testcase_dict):
""" run single testcase. """ run single testcase.
@@ -161,7 +125,12 @@ class Runner(object):
} }
@return True or raise exception during test @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") parsed_request = self.init_config(testcase_dict, level="testcase")
self.context.bind_variables({"request": parsed_request})
try: try:
url = parsed_request.pop('url') url = parsed_request.pop('url')
@@ -170,28 +139,36 @@ class Runner(object):
except KeyError: except KeyError:
raise exception.ParamsError("URL or METHOD missed!") 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_info("{method} {url}".format(method=method, url=url))
logger.log_debug("request kwargs(raw): {kwargs}".format(kwargs=parsed_request)) 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( 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)
# 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) extracted_variables_mapping = resp_obj.extract_response(extractors)
self.context.bind_extracted_variables(extracted_variables_mapping) self.context.bind_extracted_variables(extracted_variables_mapping)
# validate
validators = testcase_dict.get("validate", []) or testcase_dict.get("validators", [])
try: try:
self.context.validate(validators, resp_obj) self.context.validate(validators, resp_obj)
except (exception.ParamsError, exception.ResponseError, \ except (exception.ParamsError, exception.ResponseError, \

View File

@@ -10,7 +10,7 @@ import random
import re import re
from httprunner import exception, logger, utils 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 from httprunner.utils import FileUtils
variable_regexp = r"\$([\w_]+)" 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(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}} 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 = { function_meta = {
"func_name": matched.group(1),
"args": [], "args": [],
"kwargs": {} "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(" ", "") args_str = matched.group(2).replace(" ", "")
if args_str == "": if args_str == "":
@@ -597,6 +598,7 @@ def substitute_variables_with_mapping(content, mapping):
} }
} }
""" """
# TODO: refactor type check
if isinstance(content, bool): if isinstance(content, bool):
return content return content
@@ -903,17 +905,16 @@ class TestcaseParser(object):
return evaluated_data return evaluated_data
if isinstance(content, (numeric_types, type)): if isinstance(content, basestring):
return content
# content is in string format here # content is in string format here
content = content.strip() content = content.strip()
# replace functions with evaluated value # replace functions with evaluated value
# Notice: _eval_content_functions must be called before _eval_content_variables # Notice: _eval_content_functions must be called before _eval_content_variables
content = self._eval_content_functions(content) content = self._eval_content_functions(content)
# replace variables with binding value # replace variables with binding value
content = self._eval_content_variables(content) content = self._eval_content_variables(content)
return content return content

View File

@@ -68,8 +68,16 @@ def gen_random_string(str_len):
random_string = ''.join(random_char_list) random_string = ''.join(random_char_list)
return random_string return random_string
def setup_hook_add_kwargs(method, url, kwargs): def setup_hook_add_kwargs(request):
kwargs["key"] = "value" request["key"] = "value"
def setup_hook_remove_kwargs(method, url, kwargs): def setup_hook_remove_kwargs(request):
kwargs.pop("key") 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)

View File

@@ -9,8 +9,10 @@
url: /headers url: /headers
method: GET method: GET
setup_hooks: setup_hooks:
- setup_hook_add_kwargs - ${setup_hook_add_kwargs($request)}
- setup_hook_remove_kwargs - ${setup_hook_remove_kwargs($request)}
teardown_hooks:
- ${teardown_hook_sleep_N_secs($response, 1)}
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: [content.headers.Host, "127.0.0.1:3458"] - eq: [content.headers.Host, "127.0.0.1:3458"]

View File

@@ -40,7 +40,9 @@ class TestHttpClient(ApiServerUnittest):
self.assertEqual(True, resp.json()['success']) self.assertEqual(True, resp.json()['success'])
def test_prepare_kwargs_content_type_application_json_without_charset(self): def test_prepare_kwargs_content_type_application_json_without_charset(self):
kwargs = { request = {
"url": "/path",
"method": "POST",
"headers": { "headers": {
"content-type": "application/json" "content-type": "application/json"
}, },
@@ -49,13 +51,15 @@ class TestHttpClient(ApiServerUnittest):
"b": 2 "b": 2
} }
} }
setup_hook_prepare_kwargs("POST", "/path", kwargs) setup_hook_prepare_kwargs(request)
self.assertIsInstance(kwargs["data"], bytes) self.assertIsInstance(request["data"], bytes)
self.assertIn(b'"a": 1', kwargs["data"]) self.assertIn(b'"a": 1', request["data"])
self.assertIn(b'"b": 2', kwargs["data"]) self.assertIn(b'"b": 2', request["data"])
def test_prepare_kwargs_content_type_application_json_charset_utf8(self): def test_prepare_kwargs_content_type_application_json_charset_utf8(self):
kwargs = { request = {
"url": "/path",
"method": "POST",
"headers": { "headers": {
"content-type": "application/json; charset=utf-8" "content-type": "application/json; charset=utf-8"
}, },
@@ -64,5 +68,5 @@ class TestHttpClient(ApiServerUnittest):
"b": 2 "b": 2
} }
} }
setup_hook_prepare_kwargs("POST", "/path", kwargs) setup_hook_prepare_kwargs(request)
self.assertIsInstance(kwargs["data"], bytes) self.assertIsInstance(request["data"], bytes)

View File

@@ -231,7 +231,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 = "${teardown_hook_sleep_1_secs(1)}" content = "${sleep_N_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

@@ -60,29 +60,91 @@ class TestRunner(ApiServerUnittest):
"sign": "f1219719911caae89ccc301679857ebfda115ca2" "sign": "f1219719911caae89ccc301679857ebfda115ca2"
} }
}, },
"extract": [
{"token": "content.token"}
],
"validate": [ "validate": [
{"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_hooks": ["teardown_hook_sleep_1_secs"]
} }
with self.assertRaises(exception.ValidationError): with self.assertRaises(exception.ValidationError):
start_time = time.time()
self.test_runner.run_test(test) 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): def test_run_testset_with_setup_hooks(self):
testcase_file_path = os.path.join( testcase_file_path = os.path.join(
os.getcwd(), 'tests/httpbin/hooks.yml') os.getcwd(), 'tests/httpbin/hooks.yml')
start_time = time.time()
runner = HttpRunner().run(testcase_file_path) runner = HttpRunner().run(testcase_file_path)
end_time = time.time()
summary = runner.summary summary = runner.summary
self.assertTrue(summary["success"]) 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): def test_run_testset_hardcode(self):
for testcase_file_path in self.testcase_file_path_list: for testcase_file_path in self.testcase_file_path_list:

View File

@@ -454,6 +454,10 @@ class TestcaseParserUnittest(unittest.TestCase):
testcase.parse_function("func(1, 2, a=3, b=4)"), testcase.parse_function("func(1, 2, a=3, b=4)"),
{'func_name': 'func', 'args': [1, 2], 'kwargs': {'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): def test_parse_content_with_bindings_variables(self):
variables = { variables = {