mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 11:29:48 +08:00
add hook feature:
1, setup_hook: call function before request 2, teardown_hook: call function after request
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
28
httprunner/events.py
Normal file
28
httprunner/events.py
Normal 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user