change: http client with pydantic models

This commit is contained in:
debugtalk
2020-05-14 00:11:13 +08:00
parent 7c78f97795
commit bdd8b53d89
5 changed files with 173 additions and 166 deletions
+17 -23
View File
@@ -130,37 +130,32 @@ class HttpRunner(object):
tests_results (list): list of testcase summary tests_results (list): list of testcase summary
""" """
testsuite_summary = { testsuite_summary = TestSuiteSummary(
"success": True, success=True,
"stat": { platform=report.get_platform(),
"total": len(tests_results), testcases=[]
"success": 0, )
"fail": 0 testsuite_summary.stat.total = len(tests_results)
}, testsuite_summary.stat.success = 0
"time": {}, testsuite_summary.stat.fail = 0
"platform": report.get_platform(),
"testcases": []
}
for testcase_summary in tests_results: for testcase_summary in tests_results:
if testcase_summary.success: if testcase_summary.success:
testsuite_summary["stat"]["success"] += 1 testsuite_summary.stat.success += 1
else: else:
testsuite_summary["stat"]["fail"] += 1 testsuite_summary.stat.fail += 1
testsuite_summary["success"] &= testcase_summary.success testsuite_summary.success &= testcase_summary.success
testsuite_summary.testcases.append(testcase_summary)
testsuite_summary["testcases"].append(testcase_summary)
total_duration = tests_results[-1].time.start_at + tests_results[-1].time.duration \ total_duration = tests_results[-1].time.start_at + tests_results[-1].time.duration \
- tests_results[0].time.start_at - tests_results[0].time.start_at
testsuite_summary["time"] = {
"start_at": tests_results[0].time.start_at,
"start_at_iso_format": tests_results[0].time.start_at_iso_format,
"duration": total_duration
}
return TestSuiteSummary.parse_obj(testsuite_summary) testsuite_summary.time.start_at = tests_results[0].time.start_at
testsuite_summary.time.start_at_iso_format = tests_results[0].time.start_at_iso_format
testsuite_summary.time.duration = total_duration
return testsuite_summary
def run_tests(self, tests_mapping) -> TestSuiteSummary: def run_tests(self, tests_mapping) -> TestSuiteSummary:
""" run testcase/testsuite data """ run testcase/testsuite data
@@ -188,7 +183,6 @@ class HttpRunner(object):
# generate html report # generate html report
self.exception_stage = "generate html report" self.exception_stage = "generate html report"
report.stringify_summary(self._summary)
if self.save_tests: if self.save_tests:
utils.dump_json_file( utils.dump_json_file(
+76 -102
View File
@@ -7,81 +7,13 @@ from requests import Request, Response
from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema, from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema,
RequestException) RequestException)
from httprunner import response from httprunner.schema import RequestData, ResponseData
from httprunner.schema import SessionData, ReqRespData
from httprunner.utils import lower_dict_keys, omit_long_data from httprunner.utils import lower_dict_keys, omit_long_data
from httprunner.schema import SessionData
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def get_req_resp_record(resp_obj):
""" get request and response info from Response() object.
"""
def log_print(req_resp_dict, r_type):
msg = f"\n================== {r_type} details ==================\n"
for key, value in req_resp_dict[r_type].items():
msg += "{:<16} : {}\n".format(key, repr(value))
logger.debug(msg)
req_resp_dict = {
"request": {},
"response": {}
}
# record actual request info
req_resp_dict["request"]["url"] = resp_obj.request.url
req_resp_dict["request"]["method"] = resp_obj.request.method
req_resp_dict["request"]["headers"] = dict(resp_obj.request.headers)
request_body = resp_obj.request.body
if request_body:
request_content_type = lower_dict_keys(
req_resp_dict["request"]["headers"]
).get("content-type")
if request_content_type and "multipart/form-data" in request_content_type:
# upload file type
req_resp_dict["request"]["body"] = "upload file stream (OMITTED)"
else:
req_resp_dict["request"]["body"] = request_body
# log request details in debug mode
log_print(req_resp_dict, "request")
# record response info
req_resp_dict["response"]["ok"] = resp_obj.ok
req_resp_dict["response"]["url"] = resp_obj.url
req_resp_dict["response"]["status_code"] = resp_obj.status_code
req_resp_dict["response"]["reason"] = resp_obj.reason
req_resp_dict["response"]["cookies"] = resp_obj.cookies or {}
req_resp_dict["response"]["encoding"] = resp_obj.encoding
resp_headers = dict(resp_obj.headers)
req_resp_dict["response"]["headers"] = resp_headers
lower_resp_headers = lower_dict_keys(resp_headers)
content_type = lower_resp_headers.get("content-type", "")
req_resp_dict["response"]["content_type"] = content_type
if "image" in content_type:
# response is image type, record bytes content only
req_resp_dict["response"]["body"] = resp_obj.content
else:
try:
# try to record json data
if isinstance(resp_obj, response.ResponseObject):
req_resp_dict["response"]["body"] = resp_obj.json
else:
req_resp_dict["response"]["body"] = resp_obj.json()
except ValueError:
# only record at most 512 text charactors
resp_text = resp_obj.text
req_resp_dict["response"]["body"] = omit_long_data(resp_text)
# log response details in debug mode
log_print(req_resp_dict, "response")
return req_resp_dict
class ApiResponse(Response): class ApiResponse(Response):
def raise_for_status(self): def raise_for_status(self):
@@ -90,6 +22,73 @@ class ApiResponse(Response):
Response.raise_for_status(self) Response.raise_for_status(self)
def get_req_resp_record(resp_obj: Response) -> ReqRespData:
""" get request and response info from Response() object.
"""
def log_print(req_or_resp, r_type):
msg = f"\n================== {r_type} details ==================\n"
for key, value in req_or_resp.dict().items():
msg += "{:<16} : {}\n".format(key, repr(value))
logger.debug(msg)
# record actual request info
request_data = RequestData(
method=resp_obj.request.method,
url=resp_obj.request.url,
headers=dict(resp_obj.request.headers),
body=resp_obj.request.body
)
request_body = resp_obj.request.body
if request_body:
request_content_type = lower_dict_keys(
request_data.headers
).get("content-type")
if request_content_type and "multipart/form-data" in request_content_type:
# upload file type
request_data.body = "upload file stream (OMITTED)"
else:
request_data.body = request_body
# log request details in debug mode
log_print(request_data, "request")
# record response info
resp_headers = dict(resp_obj.headers)
lower_resp_headers = lower_dict_keys(resp_headers)
content_type = lower_resp_headers.get("content-type", "")
if "image" in content_type:
# response is image type, record bytes content only
response_body = resp_obj.content
else:
try:
# try to record json data
response_body = resp_obj.json()
except ValueError:
# only record at most 512 text charactors
resp_text = resp_obj.text
response_body = omit_long_data(resp_text)
response_data = ResponseData(
status_code=resp_obj.status_code,
cookies=resp_obj.cookies or {},
encoding=resp_obj.encoding,
headers=resp_headers,
content_type=content_type,
body=response_body
)
# log response details in debug mode
log_print(response_data, "response")
req_resp_data = ReqRespData(
request=request_data,
response=response_data
)
return req_resp_data
class HttpSession(requests.Session): class HttpSession(requests.Session):
""" """
Class for performing HTTP requests and holding (session-) cookies between requests (in order Class for performing HTTP requests and holding (session-) cookies between requests (in order
@@ -101,35 +100,15 @@ class HttpSession(requests.Session):
""" """
def __init__(self): def __init__(self):
super(HttpSession, self).__init__() super(HttpSession, self).__init__()
self.data = None self.data = SessionData()
def init_session_data(self):
""" initialize session data, it will store detail data of request and response
"""
self.data = SessionData(
req_resp=[
{
"request": {
"url": "N/A",
"method": "N/A",
"headers": {}
},
"response": {
"status_code": "N/A",
"headers": {},
"encoding": None,
"content_type": ""
}
}
]
)
def update_last_req_resp_record(self, resp_obj): def update_last_req_resp_record(self, resp_obj):
""" """
update request and response info from Response() object. update request and response info from Response() object.
""" """
self.data.req_resp.pop() # TODO: fix
self.data.req_resp.append(get_req_resp_record(resp_obj)) self.data.req_resps.pop()
self.data.req_resps.append(get_req_resp_record(resp_obj))
def request(self, method, url, name=None, **kwargs): def request(self, method, url, name=None, **kwargs):
""" """
@@ -170,15 +149,10 @@ class HttpSession(requests.Session):
:param cert: (optional) :param cert: (optional)
if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
""" """
self.init_session_data() self.data = SessionData()
# record test name
self.data.name = name
# record original request info # timeout default to 120 seconds
self.data.req_resp[0]["request"]["method"] = method
self.data.req_resp[0]["request"]["url"] = url
kwargs.setdefault("timeout", 120) kwargs.setdefault("timeout", 120)
self.data.req_resp[0]["request"].update(kwargs)
start_timestamp = time.time() start_timestamp = time.time()
response = self._send_request_safe_mode(method, url, **kwargs) response = self._send_request_safe_mode(method, url, **kwargs)
@@ -198,7 +172,7 @@ class HttpSession(requests.Session):
# record request and response histories, include 30X redirection # record request and response histories, include 30X redirection
response_list = response.history + [response] response_list = response.history + [response]
self.data.req_resp = [ self.data.req_resps = [
get_req_resp_record(resp_obj) get_req_resp_record(resp_obj)
for resp_obj in response_list for resp_obj in response_list
] ]
-16
View File
@@ -114,22 +114,6 @@ class ResponseObject(object):
} }
self.validation_results: Dict = {} self.validation_results: Dict = {}
def __getattr__(self, key):
try:
if key == "json":
value = self.resp_obj.json()
elif key == "cookies":
value = self.resp_obj.cookies.get_dict()
else:
value = getattr(self.resp_obj, key)
self.__dict__[key] = value
return value
except AttributeError:
err_msg = f"ResponseObject does not have attribute: {key}"
logger.error(err_msg)
raise ParamsError(err_msg)
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 {}
+37 -15
View File
@@ -7,7 +7,7 @@ from httprunner.client import HttpSession
from httprunner.exceptions import ValidationFailure, ParamsError from httprunner.exceptions import ValidationFailure, ParamsError
from httprunner.parser import build_url, parse_data, parse_variables_mapping from httprunner.parser import build_url, parse_data, parse_variables_mapping
from httprunner.response import ResponseObject from httprunner.response import ResponseObject
from httprunner.schema import TestsConfig, TestStep, VariablesMapping, TestCase, SessionData from httprunner.schema import TestsConfig, TestStep, VariablesMapping, TestCase, StepData
class TestCaseRunner(object): class TestCaseRunner(object):
@@ -15,9 +15,10 @@ class TestCaseRunner(object):
config: TestsConfig = {} config: TestsConfig = {}
teststeps: List[TestStep] = [] teststeps: List[TestStep] = []
session: HttpSession = None session: HttpSession = None
step_datas: List[SessionData] = [] step_datas: List[StepData] = []
validation_results: Dict = {} validation_results: Dict = {}
session_variables: Dict = {} session_variables: Dict = {}
success: bool = True # indicate testcase execution result
def init(self, testcase: TestCase) -> "TestCaseRunner": def init(self, testcase: TestCase) -> "TestCaseRunner":
self.config = testcase.config self.config = testcase.config
@@ -34,6 +35,10 @@ class TestCaseRunner(object):
def __run_step_request(self, step: TestStep): def __run_step_request(self, step: TestStep):
"""run teststep: request""" """run teststep: request"""
step_data = StepData(
name=step.name
)
# parse # parse
request_dict = step.request.dict() request_dict = step.request.dict()
parsed_request_dict = parse_data(request_dict, step.variables, self.config.functions) parsed_request_dict = parse_data(request_dict, step.variables, self.config.functions)
@@ -70,14 +75,15 @@ class TestCaseRunner(object):
# log response # log response
err_msg += "====== response details ======\n" err_msg += "====== response details ======\n"
err_msg += f"status_code: {resp_obj.status_code}\n" err_msg += f"status_code: {resp.status_code}\n"
err_msg += f"headers: {resp_obj.headers}\n" err_msg += f"headers: {resp.headers}\n"
err_msg += f"body: {repr(resp_obj.text)}\n" err_msg += f"body: {repr(resp.text)}\n"
logger.error(err_msg) logger.error(err_msg)
# extract # extract
extractors = step.extract extractors = step.extract
extract_mapping = resp_obj.extract(extractors) extract_mapping = resp_obj.extract(extractors)
step_data.export = extract_mapping
variables_mapping = step.variables variables_mapping = step.variables
variables_mapping.update(extract_mapping) variables_mapping.update(extract_mapping)
@@ -86,33 +92,49 @@ class TestCaseRunner(object):
validators = step.validators validators = step.validators
try: try:
resp_obj.validate(validators, variables_mapping, self.config.functions) resp_obj.validate(validators, variables_mapping, self.config.functions)
self.session.data.status = "passed" self.session.data.success = True
except ValidationFailure: except ValidationFailure:
self.session.data.status = "failed" self.session.data.success = False
log_req_resp_details() log_req_resp_details()
raise raise
finally: finally:
self.validation_results = resp_obj.validation_results self.validation_results = resp_obj.validation_results
# save request & response meta data # save request & response meta data
self.session.data.validators = self.validation_results self.session.data.validators = self.validation_results
self.session.data.name = step.name self.success &= self.session.data.success
self.step_datas.append(self.session.data)
return extract_mapping step_data.success = self.session.data.success
step_data.data = self.session.data
return step_data
def __run_step_testcase(self, step): def __run_step_testcase(self, step):
"""run teststep: referenced testcase""" """run teststep: referenced testcase"""
step_data = StepData(
name=step.name
)
step_variables = step.variables step_variables = step.variables
testcase: TestCaseRunner = step.testcase testcase: TestCaseRunner = step.testcase() # TODO: fix
res = testcase.with_variables(**step_variables).run() case_result = testcase.with_variables(**step_variables).run()
return res.get_export_variables() step_data.data = case_result.step_datas # list of step data
step_data.export = case_result.get_export_variables()
step_data.success = case_result.success
self.success &= case_result.success
return step_data
def __run_step(self, step: TestStep): def __run_step(self, step: TestStep):
"""run teststep, teststep maybe a request or referenced testcase"""
logger.info(f"run step: {step.name}") logger.info(f"run step: {step.name}")
if step.request: if step.request:
return self.__run_step_request(step) step_data = self.__run_step_request(step)
elif step.testcase: elif step.testcase:
return self.__run_step_testcase(step) step_data = self.__run_step_testcase(step)
else:
raise ParamsError(f"teststep is neither a request nor a referenced testcase: {step.dict()}")
self.step_datas.append(step_data)
return step_data.export
def test_start(self): def test_start(self):
"""main entrance""" """main entrance"""
+43 -10
View File
@@ -43,6 +43,7 @@ class TestsConfig(BaseModel):
class Request(BaseModel): class Request(BaseModel):
"""requests.Request model"""
method: MethodEnum = MethodEnum.GET method: MethodEnum = MethodEnum.GET
url: Url url: Url
params: Dict[Text, Text] = {} params: Dict[Text, Text] = {}
@@ -84,9 +85,9 @@ class TestsMapping(BaseModel):
class TestCaseTime(BaseModel): class TestCaseTime(BaseModel):
start_at: float start_at: float = 0
start_at_iso_format: Text start_at_iso_format: Text = ""
duration: float duration: float = 0
class TestCaseInOut(BaseModel): class TestCaseInOut(BaseModel):
@@ -100,14 +101,46 @@ class RequestStat(BaseModel):
elapsed_ms: float = 0 elapsed_ms: float = 0
class RequestData(BaseModel):
method: MethodEnum = MethodEnum.GET
url: Url
headers: Headers = {}
# TODO: add cookies
body: Union[Text, Dict] = {}
class ResponseData(BaseModel):
status_code: int
cookies: Dict
encoding: Text
headers: Dict
content_type: Text
body: Union[Text, bytes, Dict]
class ReqRespData(BaseModel):
request: RequestData
response: ResponseData
class SessionData(BaseModel): class SessionData(BaseModel):
status: Text = "" """request session data, including request, response, validators and stat data"""
name: Text = "" success: bool = False
req_resp: List[Dict] = [] # in most cases, req_resps only contains one request & response
# while when 30X redirect occurs, req_resps will contain multiple request & response
req_resps: List[ReqRespData] = []
stat: RequestStat = RequestStat() stat: RequestStat = RequestStat()
validators: Dict = {} validators: Dict = {}
class StepData(BaseModel):
"""teststep data, each step maybe corresponding to one request or one testcase"""
success: bool = False
name: Text = "" # teststep name
data: Union[SessionData, List[SessionData]] = None
export: Dict = {}
class TestCaseSummary(BaseModel): class TestCaseSummary(BaseModel):
name: Text = "" name: Text = ""
success: bool success: bool
@@ -116,7 +149,7 @@ class TestCaseSummary(BaseModel):
time: TestCaseTime time: TestCaseTime
in_out: TestCaseInOut = {} in_out: TestCaseInOut = {}
log: Text = "" log: Text = ""
step_datas: List[SessionData] = [] step_datas: List[StepData] = []
class PlatformInfo(BaseModel): class PlatformInfo(BaseModel):
@@ -132,8 +165,8 @@ class Stat(BaseModel):
class TestSuiteSummary(BaseModel): class TestSuiteSummary(BaseModel):
success: bool success: bool = False
stat: Stat stat: Stat = Stat()
time: TestCaseTime time: TestCaseTime = TestCaseTime()
platform: PlatformInfo platform: PlatformInfo
testcases: List[TestCaseSummary] testcases: List[TestCaseSummary]