mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-13 17:29:56 +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
|
||||
|
||||
## 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)
|
||||
|
||||
**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 httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
@@ -126,7 +126,7 @@ def setup_hook_httpntlmauth(request):
|
||||
def alter_response(response):
|
||||
response.status_code = 500
|
||||
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_dict = {"key": 123}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ teststeps:
|
||||
teardown_hooks:
|
||||
- ${alter_response($response)}
|
||||
validate:
|
||||
- eq: ["status_code", 200]
|
||||
# TODO: implement hooks
|
||||
# - eq: [body.headers."Content-Type", "html/text"]
|
||||
- eq: [body.headers.Host, "httpbin.org"]
|
||||
- eq: ["status_code", 500]
|
||||
- eq: [headers."Content-Type", "html/text"]
|
||||
- eq: [body.headers."Content-Type", "application/json"]
|
||||
- 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 httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
@@ -11,7 +11,10 @@ class TestCaseHooks(HttpRunner):
|
||||
Step(
|
||||
RunRequest("headers")
|
||||
.with_variables(**{"a": 123})
|
||||
.setup_hook("${setup_hook_add_kwargs($request)}")
|
||||
.setup_hook("${setup_hook_remove_kwargs($request)}")
|
||||
.get("/headers")
|
||||
.teardown_hook("${teardown_hook_sleep_N_secs($response, 1)}")
|
||||
.validate()
|
||||
.assert_equal("status_code", 200)
|
||||
.assert_contained_by("body.headers.Host", "${get_httpbin_server()}")
|
||||
@@ -19,9 +22,12 @@ class TestCaseHooks(HttpRunner):
|
||||
Step(
|
||||
RunRequest("alter response")
|
||||
.get("/headers")
|
||||
.teardown_hook("${alter_response($response)}")
|
||||
.validate()
|
||||
.assert_equal("status_code", 200)
|
||||
.assert_equal("body.headers.Host", "httpbin.org")
|
||||
.assert_equal("status_code", 500)
|
||||
.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 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 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 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 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
|
||||
|
||||
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 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 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
|
||||
|
||||
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 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 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 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."
|
||||
|
||||
from httprunner.runner import HttpRunner
|
||||
|
||||
@@ -264,7 +264,7 @@ import time
|
||||
import pytest
|
||||
from loguru import logger
|
||||
|
||||
from httprunner.utils import get_platform
|
||||
from httprunner.utils import get_platform, ExtendJSONEncoder
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
@@ -316,7 +316,7 @@ def session_fixture(request):
|
||||
os.makedirs(summary_dir, exist_ok=True)
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
@@ -60,6 +60,22 @@ except ModuleNotFoundError:
|
||||
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":
|
||||
""" preprocess for upload test
|
||||
replace `upload` info with MultipartEncoder
|
||||
@@ -86,15 +102,7 @@ def prepare_upload_step(step: TStep, functions: FunctionsMapping) -> "NoReturn":
|
||||
if not step.request.upload:
|
||||
return
|
||||
|
||||
if not 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)
|
||||
|
||||
ensure_upload_ready()
|
||||
params_list = []
|
||||
for key, value in step.request.upload.items():
|
||||
step.variables[key] = value
|
||||
@@ -126,6 +134,7 @@ def multipart_encoder(**kwargs):
|
||||
else:
|
||||
return "text/html"
|
||||
|
||||
ensure_upload_ready()
|
||||
fields_dict = {}
|
||||
for key, value in kwargs.items():
|
||||
|
||||
@@ -165,4 +174,5 @@ def multipart_content_type(m_encoder) -> Text:
|
||||
content type
|
||||
|
||||
"""
|
||||
ensure_upload_ready()
|
||||
return m_encoder.content_type
|
||||
|
||||
@@ -228,6 +228,17 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
|
||||
variables = teststep["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"):
|
||||
step_info += make_request_chain_style(teststep["request"])
|
||||
elif teststep.get("testcase"):
|
||||
@@ -235,6 +246,17 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
|
||||
call_ref_testcase = f".call({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:
|
||||
# request step
|
||||
step_info += ".extract()"
|
||||
|
||||
@@ -15,7 +15,7 @@ FunctionsMapping = Dict[Text, Callable]
|
||||
Headers = Dict[Text, Text]
|
||||
Cookies = Dict[Text, Text]
|
||||
Verify = bool
|
||||
Hook = List[Text]
|
||||
Hooks = List[Union[Text, Dict[Text, Text]]]
|
||||
Export = List[Text]
|
||||
Validators = List[Dict]
|
||||
Env = Dict[Text, Any]
|
||||
@@ -37,8 +37,8 @@ class TConfig(BaseModel):
|
||||
base_url: BaseUrl = ""
|
||||
# Text: prepare variables in debugtalk.py, ${gen_variables()}
|
||||
variables: Union[VariablesMapping, Text] = {}
|
||||
setup_hooks: Hook = []
|
||||
teardown_hooks: Hook = []
|
||||
# setup_hooks: Hooks = []
|
||||
# teardown_hooks: Hooks = []
|
||||
export: Export = []
|
||||
path: Text = None
|
||||
|
||||
@@ -64,8 +64,8 @@ class TStep(BaseModel):
|
||||
request: Union[TRequest, None] = None
|
||||
testcase: Union[Text, Callable, None] = None
|
||||
variables: VariablesMapping = {}
|
||||
setup_hooks: Hook = []
|
||||
teardown_hooks: Hook = []
|
||||
setup_hooks: Hooks = []
|
||||
teardown_hooks: Hooks = []
|
||||
# used to extract request's response field
|
||||
extract: VariablesMapping = {}
|
||||
# used to export session variables from referenced testcase
|
||||
|
||||
@@ -4,6 +4,7 @@ import jmespath
|
||||
import requests
|
||||
from loguru import logger
|
||||
|
||||
from httprunner import exceptions
|
||||
from httprunner.exceptions import ValidationFailure, ParamsError
|
||||
from httprunner.parser import parse_data, parse_string_value, get_mapping_function
|
||||
from httprunner.models import VariablesMapping, Validators, FunctionsMapping
|
||||
@@ -109,20 +110,36 @@ class ResponseObject(object):
|
||||
|
||||
"""
|
||||
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 = {}
|
||||
|
||||
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]:
|
||||
if not extractors:
|
||||
return {}
|
||||
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Text, NoReturn
|
||||
from typing import List, Dict, Text, NoReturn, Union
|
||||
|
||||
try:
|
||||
import allure
|
||||
@@ -31,6 +31,7 @@ from httprunner.models import (
|
||||
TestCaseInOut,
|
||||
ProjectMeta,
|
||||
TestCase,
|
||||
Hooks,
|
||||
)
|
||||
|
||||
|
||||
@@ -44,7 +45,7 @@ class HttpRunner(object):
|
||||
__project_meta: ProjectMeta = None
|
||||
__case_id: Text = ""
|
||||
__export: List[Text] = []
|
||||
__step_datas: List[StepData] = None
|
||||
__step_datas: List[StepData] = []
|
||||
__session: HttpSession = None
|
||||
__session_variables: VariablesMapping = {}
|
||||
# time
|
||||
@@ -86,6 +87,52 @@ class HttpRunner(object):
|
||||
self.__export = export
|
||||
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:
|
||||
"""run teststep: request"""
|
||||
step_data = StepData(name=step.name)
|
||||
@@ -101,6 +148,11 @@ class HttpRunner(object):
|
||||
"HRUN-Request-ID",
|
||||
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
|
||||
method = parsed_request_dict.pop("method")
|
||||
@@ -112,6 +164,11 @@ class HttpRunner(object):
|
||||
# request
|
||||
resp = self.__session.request(method, url, **parsed_request_dict)
|
||||
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():
|
||||
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)
|
||||
return self
|
||||
|
||||
# def hooks(self):
|
||||
# pass
|
||||
def teardown_hook(
|
||||
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:
|
||||
return StepRequestExtraction(self.__step_context)
|
||||
@@ -286,6 +293,14 @@ class RunRequest(object):
|
||||
self.__step_context.variables.update(variables)
|
||||
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:
|
||||
self.__step_context.request = TRequest(method=MethodEnum.GET, url=url)
|
||||
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}")
|
||||
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]
|
||||
name = "httprunner"
|
||||
version = "3.0.9"
|
||||
version = "3.0.10"
|
||||
description = "One-stop solution for HTTP(S) testing."
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
@@ -44,8 +44,8 @@ requests-toolbelt = {version = "^0.9.1", optional = true}
|
||||
filetype = {version = "^1.0.7", optional = true}
|
||||
|
||||
[tool.poetry.extras]
|
||||
allure = ["allure-pytest"] # poetry install -E allure
|
||||
upload = ["requests-toolbelt", "filetype"] # poetry install -E upload
|
||||
allure = ["allure-pytest"] # pip install "httprunner[allure]", poetry install -E allure
|
||||
upload = ["requests-toolbelt", "filetype"] # pip install "httprunner[upload]", poetry install -E upload
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
coverage = "^4.5.4"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import decimal
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
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):
|
||||
@@ -116,3 +118,14 @@ class TestUtils(unittest.TestCase):
|
||||
ensure_file_path_valid("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