mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-09 06:23:52 +08:00
Merge pull request #924 from httprunner/v3
## 3.0.10 (2020-06-07) **Added** - feat: implement step setup/teardown hooks - feat: support alter response in teardown hooks **Fixed** - fix: ensure upload ready - fix: add ExtendJSONEncoder to safely dump json data with python object, such as MultipartEncoder
This commit is contained in:
@@ -1,5 +1,17 @@
|
|||||||
# Release History
|
# Release History
|
||||||
|
|
||||||
|
## 3.0.10 (2020-06-07)
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- feat: implement step setup/teardown hooks
|
||||||
|
- feat: support alter response in teardown hooks
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- fix: ensure upload ready
|
||||||
|
- fix: add ExtendJSONEncoder to safely dump json data with python object, such as MultipartEncoder
|
||||||
|
|
||||||
## 3.0.9 (2020-06-07)
|
## 3.0.9 (2020-06-07)
|
||||||
|
|
||||||
**Fixed**
|
**Fixed**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/httpbin/basic.yml
|
# FROM: examples/httpbin/basic.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ def setup_hook_httpntlmauth(request):
|
|||||||
def alter_response(response):
|
def alter_response(response):
|
||||||
response.status_code = 500
|
response.status_code = 500
|
||||||
response.headers["Content-Type"] = "html/text"
|
response.headers["Content-Type"] = "html/text"
|
||||||
response.json["headers"]["Host"] = "127.0.0.1:8888"
|
response.body["headers"]["Host"] = "127.0.0.1:8888"
|
||||||
response.new_attribute = "new_attribute_value"
|
response.new_attribute = "new_attribute_value"
|
||||||
response.new_attribute_dict = {"key": 123}
|
response.new_attribute_dict = {"key": 123}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ teststeps:
|
|||||||
teardown_hooks:
|
teardown_hooks:
|
||||||
- ${alter_response($response)}
|
- ${alter_response($response)}
|
||||||
validate:
|
validate:
|
||||||
- eq: ["status_code", 200]
|
- eq: ["status_code", 500]
|
||||||
# TODO: implement hooks
|
- eq: [headers."Content-Type", "html/text"]
|
||||||
# - eq: [body.headers."Content-Type", "html/text"]
|
- eq: [body.headers."Content-Type", "application/json"]
|
||||||
- eq: [body.headers.Host, "httpbin.org"]
|
- eq: [body.headers.Host, "127.0.0.1:8888"]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/httpbin/hooks.yml
|
# FROM: examples/httpbin/hooks.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
@@ -11,7 +11,10 @@ class TestCaseHooks(HttpRunner):
|
|||||||
Step(
|
Step(
|
||||||
RunRequest("headers")
|
RunRequest("headers")
|
||||||
.with_variables(**{"a": 123})
|
.with_variables(**{"a": 123})
|
||||||
|
.setup_hook("${setup_hook_add_kwargs($request)}")
|
||||||
|
.setup_hook("${setup_hook_remove_kwargs($request)}")
|
||||||
.get("/headers")
|
.get("/headers")
|
||||||
|
.teardown_hook("${teardown_hook_sleep_N_secs($response, 1)}")
|
||||||
.validate()
|
.validate()
|
||||||
.assert_equal("status_code", 200)
|
.assert_equal("status_code", 200)
|
||||||
.assert_contained_by("body.headers.Host", "${get_httpbin_server()}")
|
.assert_contained_by("body.headers.Host", "${get_httpbin_server()}")
|
||||||
@@ -19,9 +22,12 @@ class TestCaseHooks(HttpRunner):
|
|||||||
Step(
|
Step(
|
||||||
RunRequest("alter response")
|
RunRequest("alter response")
|
||||||
.get("/headers")
|
.get("/headers")
|
||||||
|
.teardown_hook("${alter_response($response)}")
|
||||||
.validate()
|
.validate()
|
||||||
.assert_equal("status_code", 200)
|
.assert_equal("status_code", 500)
|
||||||
.assert_equal("body.headers.Host", "httpbin.org")
|
.assert_equal('headers."Content-Type"', "html/text")
|
||||||
|
.assert_equal('body.headers."Content-Type"', "application/json")
|
||||||
|
.assert_equal("body.headers.Host", "127.0.0.1:8888")
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/httpbin/load_image.yml
|
# FROM: examples/httpbin/load_image.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/httpbin/upload.yml
|
# FROM: examples/httpbin/upload.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/httpbin/validate.yml
|
# FROM: examples/httpbin/validate.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
|
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
|
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/postman_echo/request_methods/hardcode.yml
|
# FROM: examples/postman_echo/request_methods/hardcode.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
|
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
|
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/postman_echo/request_methods/request_with_variables.yml
|
# FROM: examples/postman_echo/request_methods/request_with_variables.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/postman_echo/request_methods/validate_with_functions.yml
|
# FROM: examples/postman_echo/request_methods/validate_with_functions.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NOTE: Generated By HttpRunner v3.0.9
|
# NOTE: Generated By HttpRunner v3.0.10
|
||||||
# FROM: examples/postman_echo/request_methods/validate_with_variables.yml
|
# FROM: examples/postman_echo/request_methods/validate_with_variables.yml
|
||||||
|
|
||||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
__version__ = "3.0.9"
|
__version__ = "3.0.10"
|
||||||
__description__ = "One-stop solution for HTTP(S) testing."
|
__description__ = "One-stop solution for HTTP(S) testing."
|
||||||
|
|
||||||
from httprunner.runner import HttpRunner
|
from httprunner.runner import HttpRunner
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ import time
|
|||||||
import pytest
|
import pytest
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from httprunner.utils import get_platform
|
from httprunner.utils import get_platform, ExtendJSONEncoder
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
@@ -316,7 +316,7 @@ def session_fixture(request):
|
|||||||
os.makedirs(summary_dir, exist_ok=True)
|
os.makedirs(summary_dir, exist_ok=True)
|
||||||
|
|
||||||
with open(summary_path, "w", encoding="utf-8") as f:
|
with open(summary_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(summary, f, indent=4)
|
json.dump(summary, f, indent=4, ensure_ascii=False, cls=ExtendJSONEncoder)
|
||||||
|
|
||||||
logger.info(f"generated task summary: {summary_path}")
|
logger.info(f"generated task summary: {summary_path}")
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,22 @@ except ModuleNotFoundError:
|
|||||||
UPLOAD_READY = False
|
UPLOAD_READY = False
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_upload_ready():
|
||||||
|
if UPLOAD_READY:
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = """
|
||||||
|
uploader extension dependencies uninstalled, install first and try again.
|
||||||
|
install with pip:
|
||||||
|
$ pip install requests_toolbelt filetype
|
||||||
|
|
||||||
|
or you can install httprunner with optional upload dependencies:
|
||||||
|
$ pip install "httprunner[upload]"
|
||||||
|
"""
|
||||||
|
logger.error(msg)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def prepare_upload_step(step: TStep, functions: FunctionsMapping) -> "NoReturn":
|
def prepare_upload_step(step: TStep, functions: FunctionsMapping) -> "NoReturn":
|
||||||
""" preprocess for upload test
|
""" preprocess for upload test
|
||||||
replace `upload` info with MultipartEncoder
|
replace `upload` info with MultipartEncoder
|
||||||
@@ -86,15 +102,7 @@ def prepare_upload_step(step: TStep, functions: FunctionsMapping) -> "NoReturn":
|
|||||||
if not step.request.upload:
|
if not step.request.upload:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not UPLOAD_READY:
|
ensure_upload_ready()
|
||||||
msg = """
|
|
||||||
uploader extension dependencies uninstalled, install first and try again.
|
|
||||||
install with pip:
|
|
||||||
$ pip install requests_toolbelt filetype
|
|
||||||
"""
|
|
||||||
logger.error(msg)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
params_list = []
|
params_list = []
|
||||||
for key, value in step.request.upload.items():
|
for key, value in step.request.upload.items():
|
||||||
step.variables[key] = value
|
step.variables[key] = value
|
||||||
@@ -126,6 +134,7 @@ def multipart_encoder(**kwargs):
|
|||||||
else:
|
else:
|
||||||
return "text/html"
|
return "text/html"
|
||||||
|
|
||||||
|
ensure_upload_ready()
|
||||||
fields_dict = {}
|
fields_dict = {}
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
|
|
||||||
@@ -165,4 +174,5 @@ def multipart_content_type(m_encoder) -> Text:
|
|||||||
content type
|
content type
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
ensure_upload_ready()
|
||||||
return m_encoder.content_type
|
return m_encoder.content_type
|
||||||
|
|||||||
@@ -228,6 +228,17 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
|
|||||||
variables = teststep["variables"]
|
variables = teststep["variables"]
|
||||||
step_info += f".with_variables(**{variables})"
|
step_info += f".with_variables(**{variables})"
|
||||||
|
|
||||||
|
if "setup_hooks" in teststep:
|
||||||
|
setup_hooks = teststep["setup_hooks"]
|
||||||
|
for hook in setup_hooks:
|
||||||
|
if isinstance(hook, Text):
|
||||||
|
step_info += f'.setup_hook("{hook}")'
|
||||||
|
elif isinstance(hook, Dict) and len(hook) == 1:
|
||||||
|
assign_var_name, hook_content = list(hook.items())[0]
|
||||||
|
step_info += f'.setup_hook("{hook}", "{assign_var_name}")'
|
||||||
|
else:
|
||||||
|
raise exceptions.TestCaseFormatError(f"Invalid setup hook: {hook}")
|
||||||
|
|
||||||
if teststep.get("request"):
|
if teststep.get("request"):
|
||||||
step_info += make_request_chain_style(teststep["request"])
|
step_info += make_request_chain_style(teststep["request"])
|
||||||
elif teststep.get("testcase"):
|
elif teststep.get("testcase"):
|
||||||
@@ -235,6 +246,17 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
|
|||||||
call_ref_testcase = f".call({testcase})"
|
call_ref_testcase = f".call({testcase})"
|
||||||
step_info += call_ref_testcase
|
step_info += call_ref_testcase
|
||||||
|
|
||||||
|
if "teardown_hooks" in teststep:
|
||||||
|
teardown_hooks = teststep["teardown_hooks"]
|
||||||
|
for hook in teardown_hooks:
|
||||||
|
if isinstance(hook, Text):
|
||||||
|
step_info += f'.teardown_hook("{hook}")'
|
||||||
|
elif isinstance(hook, Dict) and len(hook) == 1:
|
||||||
|
assign_var_name, hook_content = list(hook.items())[0]
|
||||||
|
step_info += f'.teardown_hook("{hook}", "{assign_var_name}")'
|
||||||
|
else:
|
||||||
|
raise exceptions.TestCaseFormatError(f"Invalid teardown hook: {hook}")
|
||||||
|
|
||||||
if "extract" in teststep:
|
if "extract" in teststep:
|
||||||
# request step
|
# request step
|
||||||
step_info += ".extract()"
|
step_info += ".extract()"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ FunctionsMapping = Dict[Text, Callable]
|
|||||||
Headers = Dict[Text, Text]
|
Headers = Dict[Text, Text]
|
||||||
Cookies = Dict[Text, Text]
|
Cookies = Dict[Text, Text]
|
||||||
Verify = bool
|
Verify = bool
|
||||||
Hook = List[Text]
|
Hooks = List[Union[Text, Dict[Text, Text]]]
|
||||||
Export = List[Text]
|
Export = List[Text]
|
||||||
Validators = List[Dict]
|
Validators = List[Dict]
|
||||||
Env = Dict[Text, Any]
|
Env = Dict[Text, Any]
|
||||||
@@ -37,8 +37,8 @@ class TConfig(BaseModel):
|
|||||||
base_url: BaseUrl = ""
|
base_url: BaseUrl = ""
|
||||||
# Text: prepare variables in debugtalk.py, ${gen_variables()}
|
# Text: prepare variables in debugtalk.py, ${gen_variables()}
|
||||||
variables: Union[VariablesMapping, Text] = {}
|
variables: Union[VariablesMapping, Text] = {}
|
||||||
setup_hooks: Hook = []
|
# setup_hooks: Hooks = []
|
||||||
teardown_hooks: Hook = []
|
# teardown_hooks: Hooks = []
|
||||||
export: Export = []
|
export: Export = []
|
||||||
path: Text = None
|
path: Text = None
|
||||||
|
|
||||||
@@ -64,8 +64,8 @@ class TStep(BaseModel):
|
|||||||
request: Union[TRequest, None] = None
|
request: Union[TRequest, None] = None
|
||||||
testcase: Union[Text, Callable, None] = None
|
testcase: Union[Text, Callable, None] = None
|
||||||
variables: VariablesMapping = {}
|
variables: VariablesMapping = {}
|
||||||
setup_hooks: Hook = []
|
setup_hooks: Hooks = []
|
||||||
teardown_hooks: Hook = []
|
teardown_hooks: Hooks = []
|
||||||
# used to extract request's response field
|
# used to extract request's response field
|
||||||
extract: VariablesMapping = {}
|
extract: VariablesMapping = {}
|
||||||
# used to export session variables from referenced testcase
|
# used to export session variables from referenced testcase
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import jmespath
|
|||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from httprunner import exceptions
|
||||||
from httprunner.exceptions import ValidationFailure, ParamsError
|
from httprunner.exceptions import ValidationFailure, ParamsError
|
||||||
from httprunner.parser import parse_data, parse_string_value, get_mapping_function
|
from httprunner.parser import parse_data, parse_string_value, get_mapping_function
|
||||||
from httprunner.models import VariablesMapping, Validators, FunctionsMapping
|
from httprunner.models import VariablesMapping, Validators, FunctionsMapping
|
||||||
@@ -109,20 +110,36 @@ class ResponseObject(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
self.resp_obj = resp_obj
|
self.resp_obj = resp_obj
|
||||||
|
|
||||||
try:
|
|
||||||
body = resp_obj.json()
|
|
||||||
except ValueError:
|
|
||||||
body = resp_obj.content
|
|
||||||
|
|
||||||
self.resp_obj_meta = {
|
|
||||||
"status_code": resp_obj.status_code,
|
|
||||||
"headers": resp_obj.headers,
|
|
||||||
"cookies": dict(resp_obj.cookies),
|
|
||||||
"body": body,
|
|
||||||
}
|
|
||||||
self.validation_results: Dict = {}
|
self.validation_results: Dict = {}
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
if key in ["json", "content", "body"]:
|
||||||
|
try:
|
||||||
|
value = self.resp_obj.json()
|
||||||
|
except ValueError:
|
||||||
|
value = self.resp_obj.content
|
||||||
|
elif key == "cookies":
|
||||||
|
value = self.resp_obj.cookies.get_dict()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
value = getattr(self.resp_obj, key)
|
||||||
|
except AttributeError:
|
||||||
|
err_msg = "ResponseObject does not have attribute: {}".format(key)
|
||||||
|
logger.error(err_msg)
|
||||||
|
raise exceptions.ParamsError(err_msg)
|
||||||
|
|
||||||
|
self.__dict__[key] = value
|
||||||
|
return value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def resp_obj_meta(self):
|
||||||
|
return {
|
||||||
|
"status_code": self.status_code,
|
||||||
|
"headers": self.headers,
|
||||||
|
"cookies": self.cookies,
|
||||||
|
"body": self.body,
|
||||||
|
}
|
||||||
|
|
||||||
def extract(self, extractors: Dict[Text, Text]) -> Dict[Text, Any]:
|
def extract(self, extractors: Dict[Text, Text]) -> Dict[Text, Any]:
|
||||||
if not extractors:
|
if not extractors:
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Dict, Text, NoReturn
|
from typing import List, Dict, Text, NoReturn, Union
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import allure
|
import allure
|
||||||
@@ -31,6 +31,7 @@ from httprunner.models import (
|
|||||||
TestCaseInOut,
|
TestCaseInOut,
|
||||||
ProjectMeta,
|
ProjectMeta,
|
||||||
TestCase,
|
TestCase,
|
||||||
|
Hooks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ class HttpRunner(object):
|
|||||||
__project_meta: ProjectMeta = None
|
__project_meta: ProjectMeta = None
|
||||||
__case_id: Text = ""
|
__case_id: Text = ""
|
||||||
__export: List[Text] = []
|
__export: List[Text] = []
|
||||||
__step_datas: List[StepData] = None
|
__step_datas: List[StepData] = []
|
||||||
__session: HttpSession = None
|
__session: HttpSession = None
|
||||||
__session_variables: VariablesMapping = {}
|
__session_variables: VariablesMapping = {}
|
||||||
# time
|
# time
|
||||||
@@ -86,6 +87,52 @@ class HttpRunner(object):
|
|||||||
self.__export = export
|
self.__export = export
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def __call_hooks(
|
||||||
|
self, hooks: Hooks, step_variables: VariablesMapping, hook_type: Text,
|
||||||
|
) -> NoReturn:
|
||||||
|
""" call hook actions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hooks (list): each hook in hooks list maybe in two format.
|
||||||
|
|
||||||
|
format1 (str): only call hook functions.
|
||||||
|
${func()}
|
||||||
|
format2 (dict): assignment, the value returned by hook function will be assigned to variable.
|
||||||
|
{"var": "${func()}"}
|
||||||
|
|
||||||
|
step_variables: current step variables to call hook, include two special variables
|
||||||
|
|
||||||
|
request: parsed request dict
|
||||||
|
response: ResponseObject for current response
|
||||||
|
|
||||||
|
hook_type: setup/teardown
|
||||||
|
|
||||||
|
"""
|
||||||
|
logger.debug(f"call {hook_type} hook actions.")
|
||||||
|
|
||||||
|
if not isinstance(hooks, List):
|
||||||
|
logger.error(f"Invalid hooks format: {hooks}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for hook in hooks:
|
||||||
|
if isinstance(hook, Text):
|
||||||
|
# format 1: ["${func()}"]
|
||||||
|
logger.debug(f"call hook function: {hook}")
|
||||||
|
parse_data(hook, step_variables, self.__project_meta.functions)
|
||||||
|
elif isinstance(hook, Dict) and len(hook) == 1:
|
||||||
|
# format 2: {"var": "${func()}"}
|
||||||
|
var_name, hook_content = list(hook.items())[0]
|
||||||
|
hook_content_eval = parse_data(
|
||||||
|
hook_content, step_variables, self.__project_meta.functions
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"call hook function: {hook_content}, got value: {hook_content_eval}"
|
||||||
|
)
|
||||||
|
logger.debug(f"assign variable: {var_name} = {hook_content_eval}")
|
||||||
|
step_variables[var_name] = hook_content_eval
|
||||||
|
else:
|
||||||
|
logger.error(f"Invalid hook format: {hook}")
|
||||||
|
|
||||||
def __run_step_request(self, step: TStep) -> StepData:
|
def __run_step_request(self, step: TStep) -> StepData:
|
||||||
"""run teststep: request"""
|
"""run teststep: request"""
|
||||||
step_data = StepData(name=step.name)
|
step_data = StepData(name=step.name)
|
||||||
@@ -101,6 +148,11 @@ class HttpRunner(object):
|
|||||||
"HRUN-Request-ID",
|
"HRUN-Request-ID",
|
||||||
f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}",
|
f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}",
|
||||||
)
|
)
|
||||||
|
step.variables["request"] = parsed_request_dict
|
||||||
|
|
||||||
|
# setup hooks
|
||||||
|
if step.setup_hooks:
|
||||||
|
self.__call_hooks(step.setup_hooks, step.variables, "setup")
|
||||||
|
|
||||||
# prepare arguments
|
# prepare arguments
|
||||||
method = parsed_request_dict.pop("method")
|
method = parsed_request_dict.pop("method")
|
||||||
@@ -112,6 +164,11 @@ class HttpRunner(object):
|
|||||||
# request
|
# request
|
||||||
resp = self.__session.request(method, url, **parsed_request_dict)
|
resp = self.__session.request(method, url, **parsed_request_dict)
|
||||||
resp_obj = ResponseObject(resp)
|
resp_obj = ResponseObject(resp)
|
||||||
|
step.variables["response"] = resp_obj
|
||||||
|
|
||||||
|
# teardown hooks
|
||||||
|
if step.teardown_hooks:
|
||||||
|
self.__call_hooks(step.teardown_hooks, step.variables, "teardown")
|
||||||
|
|
||||||
def log_req_resp_details():
|
def log_req_resp_details():
|
||||||
err_msg = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
|
err_msg = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
|
||||||
|
|||||||
@@ -265,8 +265,15 @@ class RequestWithOptionalArgs(object):
|
|||||||
self.__step_context.request.upload.update(file_info)
|
self.__step_context.request.upload.update(file_info)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# def hooks(self):
|
def teardown_hook(
|
||||||
# pass
|
self, hook: Text, assign_var_name: Text = None
|
||||||
|
) -> "RequestWithOptionalArgs":
|
||||||
|
if assign_var_name:
|
||||||
|
self.__step_context.teardown_hooks.append({assign_var_name: hook})
|
||||||
|
else:
|
||||||
|
self.__step_context.teardown_hooks.append(hook)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
def extract(self) -> StepRequestExtraction:
|
def extract(self) -> StepRequestExtraction:
|
||||||
return StepRequestExtraction(self.__step_context)
|
return StepRequestExtraction(self.__step_context)
|
||||||
@@ -286,6 +293,14 @@ class RunRequest(object):
|
|||||||
self.__step_context.variables.update(variables)
|
self.__step_context.variables.update(variables)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunRequest":
|
||||||
|
if assign_var_name:
|
||||||
|
self.__step_context.setup_hooks.append({assign_var_name: hook})
|
||||||
|
else:
|
||||||
|
self.__step_context.setup_hooks.append(hook)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
def get(self, url: Text) -> RequestWithOptionalArgs:
|
def get(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
self.__step_context.request = TRequest(method=MethodEnum.GET, url=url)
|
self.__step_context.request = TRequest(method=MethodEnum.GET, url=url)
|
||||||
return RequestWithOptionalArgs(self.__step_context)
|
return RequestWithOptionalArgs(self.__step_context)
|
||||||
|
|||||||
@@ -212,3 +212,14 @@ def ensure_file_path_valid(file_path: Text) -> Text:
|
|||||||
|
|
||||||
new_file_path = os.path.join(os.getcwd(), f"{os.sep.join(path_names)}{file_suffix}")
|
new_file_path = os.path.join(os.getcwd(), f"{os.sep.join(path_names)}{file_suffix}")
|
||||||
return new_file_path
|
return new_file_path
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendJSONEncoder(json.JSONEncoder):
|
||||||
|
""" especially used to safely dump json data with python object, such as MultipartEncoder
|
||||||
|
"""
|
||||||
|
|
||||||
|
def default(self, obj):
|
||||||
|
try:
|
||||||
|
return super(ExtendJSONEncoder, self).default(obj)
|
||||||
|
except (UnicodeDecodeError, TypeError):
|
||||||
|
return repr(obj)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "httprunner"
|
name = "httprunner"
|
||||||
version = "3.0.9"
|
version = "3.0.10"
|
||||||
description = "One-stop solution for HTTP(S) testing."
|
description = "One-stop solution for HTTP(S) testing."
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -44,8 +44,8 @@ requests-toolbelt = {version = "^0.9.1", optional = true}
|
|||||||
filetype = {version = "^1.0.7", optional = true}
|
filetype = {version = "^1.0.7", optional = true}
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
allure = ["allure-pytest"] # poetry install -E allure
|
allure = ["allure-pytest"] # pip install "httprunner[allure]", poetry install -E allure
|
||||||
upload = ["requests-toolbelt", "filetype"] # poetry install -E upload
|
upload = ["requests-toolbelt", "filetype"] # pip install "httprunner[upload]", poetry install -E upload
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
coverage = "^4.5.4"
|
coverage = "^4.5.4"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import decimal
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from httprunner import loader, utils
|
from httprunner import loader, utils
|
||||||
from httprunner.utils import ensure_file_path_valid
|
from httprunner.utils import ensure_file_path_valid, ExtendJSONEncoder
|
||||||
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
class TestUtils(unittest.TestCase):
|
||||||
@@ -116,3 +118,14 @@ class TestUtils(unittest.TestCase):
|
|||||||
ensure_file_path_valid("examples/postman_echo/request_methods/"),
|
ensure_file_path_valid("examples/postman_echo/request_methods/"),
|
||||||
os.path.join(os.getcwd(), "examples/postman_echo/request_methods"),
|
os.path.join(os.getcwd(), "examples/postman_echo/request_methods"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_safe_dump_json(self):
|
||||||
|
class A(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
data = {"a": A(), "b": decimal.Decimal("1.45")}
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
json.dumps(data)
|
||||||
|
|
||||||
|
json.dumps(data, cls=ExtendJSONEncoder)
|
||||||
|
|||||||
Reference in New Issue
Block a user