diff --git a/httprunner/cli.py b/httprunner/cli.py
index 671969a3..2523c17e 100644
--- a/httprunner/cli.py
+++ b/httprunner/cli.py
@@ -73,15 +73,15 @@ def main_run(args):
err_code = 0
try:
for path in args.testfile_paths:
- summary = runner.run_path(path, dot_env_path=args.dot_env_path)
+ testsuite_summary = runner.run_path(path, dot_env_path=args.dot_env_path)
report_dir = args.report_dir or os.path.join(os.getcwd(), "reports")
gen_html_report(
- summary,
+ testsuite_summary,
report_template=args.report_template,
report_dir=report_dir,
report_file=args.report_file
)
- err_code |= (0 if summary and summary["success"] else 1)
+ err_code |= (0 if testsuite_summary and testsuite_summary.success else 1)
except Exception as ex:
logger.error(f"!!!!!!!!!! exception stage: {runner.exception_stage} !!!!!!!!!!\n{str(ex)}")
err_code = 1
diff --git a/httprunner/client.py b/httprunner/client.py
index 4577a40a..41e12129 100644
--- a/httprunner/client.py
+++ b/httprunner/client.py
@@ -11,6 +11,7 @@ from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema,
from httprunner import response
from httprunner.utils import lower_dict_keys, omit_long_data
+from httprunner.v3.schema import MetaData, RequestStat
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@@ -107,9 +108,8 @@ class HttpSession(requests.Session):
def init_meta_data(self):
""" initialize meta_data, it will store detail data of request and response
"""
- self.meta_data = {
- "name": "",
- "data": [
+ self.meta_data = MetaData(
+ data=[
{
"request": {
"url": "N/A",
@@ -124,19 +124,15 @@ class HttpSession(requests.Session):
}
}
],
- "stat": {
- "content_size": "N/A",
- "response_time_ms": "N/A",
- "elapsed_ms": "N/A",
- }
- }
+ stat=RequestStat()
+ )
def update_last_req_resp_record(self, resp_obj):
"""
update request and response info from Response() object.
"""
- self.meta_data["data"].pop()
- self.meta_data["data"].append(get_req_resp_record(resp_obj))
+ self.meta_data.data.pop()
+ self.meta_data.data.append(get_req_resp_record(resp_obj))
def request(self, method, url, name=None, **kwargs):
"""
@@ -180,13 +176,13 @@ class HttpSession(requests.Session):
self.init_meta_data()
# record test name
- self.meta_data["name"] = name
+ self.meta_data.name = name
# record original request info
- self.meta_data["data"][0]["request"]["method"] = method
- self.meta_data["data"][0]["request"]["url"] = url
+ self.meta_data.data[0]["request"]["method"] = method
+ self.meta_data.data[0]["request"]["url"] = url
kwargs.setdefault("timeout", 120)
- self.meta_data["data"][0]["request"].update(kwargs)
+ self.meta_data.data[0]["request"].update(kwargs)
start_timestamp = time.time()
response = self._send_request_safe_mode(method, url, **kwargs)
@@ -200,15 +196,13 @@ class HttpSession(requests.Session):
content_size = len(response.content or "")
# record the consumed time
- self.meta_data["stat"] = {
- "response_time_ms": response_time_ms,
- "elapsed_ms": response.elapsed.microseconds / 1000.0,
- "content_size": content_size
- }
+ self.meta_data.stat.response_time_ms = response_time_ms
+ self.meta_data.stat.elapsed_ms = response.elapsed.microseconds / 1000.0
+ self.meta_data.stat.content_size = content_size
# record request and response histories, include 30X redirection
response_list = response.history + [response]
- self.meta_data["data"] = [
+ self.meta_data.data = [
get_req_resp_record(resp_obj)
for resp_obj in response_list
]
diff --git a/httprunner/report/html/gen_report.py b/httprunner/report/html/gen_report.py
index c7791183..95772079 100644
--- a/httprunner/report/html/gen_report.py
+++ b/httprunner/report/html/gen_report.py
@@ -6,20 +6,21 @@ from jinja2 import Template
from loguru import logger
from httprunner.exceptions import SummaryEmpty
+from httprunner.v3.schema import TestSuiteSummary
-def gen_html_report(summary, report_template=None, report_dir=None, report_file=None):
+def gen_html_report(testsuite_summary: TestSuiteSummary, report_template=None, report_dir=None, report_file=None):
""" render html report with specified report name and template
Args:
- summary (dict): test result summary data
+ testsuite_summary (dict): testsuite result summary data
report_template (str): specify html report template path, template should be in Jinja2 format.
report_dir (str): specify html report save directory
report_file (str): specify html report file path, this has higher priority than specifying report dir.
"""
- if not summary["time"] or summary["stat"]["testcases"]["total"] == 0:
- logger.error(f"test result summary is empty ! {summary}")
+ if not testsuite_summary.time or testsuite_summary.stat.testcases["total"] == 0:
+ logger.error(f"test result testsuite_summary is empty ! {testsuite_summary}")
raise SummaryEmpty
if not report_template:
@@ -33,9 +34,9 @@ def gen_html_report(summary, report_template=None, report_dir=None, report_file=
logger.info("Start to render Html report ...")
- start_at_timestamp = summary["time"]["start_at"]
+ start_at_timestamp = testsuite_summary.time.start_at
utc_time_iso_8601_str = datetime.utcfromtimestamp(start_at_timestamp).isoformat()
- summary["time"]["start_datetime"] = utc_time_iso_8601_str
+ testsuite_summary.time.start_datetime = utc_time_iso_8601_str
if report_file:
report_dir = os.path.dirname(report_file)
@@ -55,7 +56,7 @@ def gen_html_report(summary, report_template=None, report_dir=None, report_file=
rendered_content = Template(
template_content,
extensions=["jinja2.ext.loopcontrols"]
- ).render(summary)
+ ).render(testsuite_summary.dict())
fp_w.write(rendered_content)
logger.info(f"Generated Html report: {report_path}")
diff --git a/httprunner/report/html/result.py b/httprunner/report/html/result.py
index 762d0bb1..e9d88f2f 100644
--- a/httprunner/report/html/result.py
+++ b/httprunner/report/html/result.py
@@ -3,6 +3,8 @@ import unittest
from loguru import logger
+from httprunner.v3.schema import Record
+
class HtmlTestResult(unittest.TextTestResult):
""" A html result class that can generate formatted html results.
@@ -13,13 +15,13 @@ class HtmlTestResult(unittest.TextTestResult):
self.records = []
def _record_test(self, test, status, attachment=''):
- data = {
- 'name': test.shortDescription(),
- 'status': status,
- 'attachment': attachment,
- "meta_datas": test.meta_datas
- }
- self.records.append(data)
+ record = Record(
+ name=test.shortDescription(),
+ status=status,
+ attachment=attachment,
+ meta_datas=test.meta_datas
+ )
+ self.records.append(record)
def startTestRun(self):
self.start_at = time.time()
diff --git a/httprunner/report/html/template.html b/httprunner/report/html/template.html
index 8bbfc1bf..a205a73f 100644
--- a/httprunner/report/html/template.html
+++ b/httprunner/report/html/template.html
@@ -201,7 +201,7 @@
{% for record in test_suite_summary.records %}
{% set record_index = "{}_{}".format(suite_index, loop.index) %}
- {% set record_meta_datas = record.meta_datas_expanded %}
+ {% set record_meta_datas = record.meta_datas %}
| {{record.status}} |
{{record.name}} |
diff --git a/httprunner/report/stringify.py b/httprunner/report/stringify.py
index c6b9cf11..d13e2062 100644
--- a/httprunner/report/stringify.py
+++ b/httprunner/report/stringify.py
@@ -1,10 +1,13 @@
import json
from base64 import b64encode
from collections import Iterable
+from typing import List
from jinja2 import escape
from requests.cookies import RequestsCookieJar
+from httprunner.v3.schema import TestSuiteSummary, MetaData
+
def dumps_json(value):
""" dumps json value to indented string
@@ -164,20 +167,20 @@ def __expand_meta_datas(meta_datas, meta_datas_expanded):
[dict1, dict2, dict3]
"""
- if isinstance(meta_datas, dict):
+ if isinstance(meta_datas, MetaData):
meta_datas_expanded.append(meta_datas)
elif isinstance(meta_datas, list):
for meta_data in meta_datas:
__expand_meta_datas(meta_data, meta_datas_expanded)
-def __get_total_response_time(meta_datas_expanded):
+def __get_total_response_time(meta_datas: List[MetaData]):
""" caculate total response time of all meta_datas
"""
try:
response_time = 0
- for meta_data in meta_datas_expanded:
- response_time += meta_data["stat"]["response_time_ms"]
+ for meta_data in meta_datas:
+ response_time += meta_data.stat.response_time_ms
return "{:.2f}".format(response_time)
@@ -186,30 +189,24 @@ def __get_total_response_time(meta_datas_expanded):
return "N/A"
-def __stringify_meta_datas(meta_datas):
+def __stringify_meta_datas(meta_datas: List[MetaData]):
- if isinstance(meta_datas, list):
- for _meta_data in meta_datas:
- __stringify_meta_datas(_meta_data)
- elif isinstance(meta_datas, dict):
- data_list = meta_datas["data"]
+ for meta_data in meta_datas:
+ data_list = meta_data.data
for data in data_list:
__stringify_request(data["request"])
__stringify_response(data["response"])
-def stringify_summary(summary):
+def stringify_summary(testsuite_summary: TestSuiteSummary):
""" stringify summary, in order to dump json file and generate html report.
"""
- for index, suite_summary in enumerate(summary["details"]):
+ for index, testcase_summary in enumerate(testsuite_summary.details):
- if not suite_summary.get("name"):
- suite_summary["name"] = f"testcase {index}"
+ if not testcase_summary.name:
+ testcase_summary.name = f"testcase {index}"
- for record in suite_summary.get("records"):
- meta_datas = record['meta_datas']
+ for record in testcase_summary.records:
+ meta_datas = record.meta_datas
__stringify_meta_datas(meta_datas)
- meta_datas_expanded = []
- __expand_meta_datas(meta_datas, meta_datas_expanded)
- record["meta_datas_expanded"] = meta_datas_expanded
- record["response_time"] = __get_total_response_time(meta_datas_expanded)
+ record.response_time = __get_total_response_time(meta_datas)
diff --git a/httprunner/report/summarize.py b/httprunner/report/summarize.py
index 93c7145f..404d8cb4 100644
--- a/httprunner/report/summarize.py
+++ b/httprunner/report/summarize.py
@@ -1,6 +1,8 @@
import platform
from httprunner import __version__
+from httprunner.report.html.result import HtmlTestResult
+from httprunner.v3.schema import TestCaseSummary, TestCaseStat, TestCaseTime, TestCaseInOut
def get_platform():
@@ -38,7 +40,7 @@ def aggregate_stat(origin_stat, new_stat):
origin_stat[key] += new_stat[key]
-def get_summary(result):
+def get_summary(result: HtmlTestResult) -> TestCaseSummary:
""" get summary from test result
Args:
@@ -55,28 +57,20 @@ def get_summary(result):
}
"""
- summary = {
- "success": result.wasSuccessful(),
- "stat": {
- 'total': result.testsRun,
- 'failures': len(result.failures),
- 'errors': len(result.errors),
- 'skipped': len(result.skipped),
- 'expectedFailures': len(result.expectedFailures),
- 'unexpectedSuccesses': len(result.unexpectedSuccesses)
- }
- }
- summary["stat"]["successes"] = summary["stat"]["total"] \
- - summary["stat"]["failures"] \
- - summary["stat"]["errors"] \
- - summary["stat"]["skipped"] \
- - summary["stat"]["expectedFailures"] \
- - summary["stat"]["unexpectedSuccesses"]
-
- summary["time"] = {
- 'start_at': result.start_at,
- 'duration': result.duration
- }
- summary["records"] = result.records
-
- return summary
+ return TestCaseSummary(
+ success=result.wasSuccessful(),
+ stat=TestCaseStat(
+ total=result.testsRun,
+ failures=len(result.failures),
+ errors=len(result.errors),
+ skipped=len(result.skipped),
+ expectedFailures=len(result.expectedFailures),
+ unexpectedSuccesses=len(result.unexpectedSuccesses)
+ ),
+ time=TestCaseTime(
+ start_at=result.start_at,
+ duration=result.duration
+ ),
+ records=result.records,
+ in_out=TestCaseInOut()
+ )
diff --git a/httprunner/v3/api.py b/httprunner/v3/api.py
index fb1708c9..b7a11d63 100644
--- a/httprunner/v3/api.py
+++ b/httprunner/v3/api.py
@@ -7,7 +7,7 @@ from loguru import logger
from httprunner import report, loader, utils, exceptions, __version__
from httprunner.v3.runner import TestCaseRunner
-from httprunner.v3.schema import TestsMapping
+from httprunner.v3.schema import TestsMapping, TestCaseSummary, TestSuiteSummary
class HttpRunner(object):
@@ -91,10 +91,10 @@ class HttpRunner(object):
return prepared_testcases
- def _run_suite(self, prepared_testcases: List[unittest.TestSuite]) -> List[Dict]:
+ def _run_suite(self, prepared_testcases: List[unittest.TestSuite]) -> List[TestCaseSummary]:
""" run prepared testcases
"""
- tests_results: List[Dict] = []
+ tests_results: List[TestCaseSummary] = []
for index, testcase in enumerate(prepared_testcases):
log_handler = None
@@ -108,18 +108,16 @@ class HttpRunner(object):
result = self.unittest_runner.run(testcase)
testcase_summary = report.get_summary(result)
- testcase_summary["name"] = testcase.config.name
- testcase_summary["in_out"] = {
- "in": testcase.config.variables,
- "out": testcase.config.export
- }
+ testcase_summary.name = testcase.config.name
+ testcase_summary.in_out.vars = testcase.config.variables
+ testcase_summary.in_out.out = testcase.config.export
if self.save_tests and log_handler:
logger.remove(log_handler)
logs_file_abs_path = utils.prepare_log_file_abs_path(
self.test_path, f"testcase_{index+1}.log"
)
- testcase_summary["log"] = logs_file_abs_path
+ testcase_summary.log = logs_file_abs_path
if result.wasSuccessful():
tests_results.append(testcase_summary)
@@ -128,14 +126,14 @@ class HttpRunner(object):
return tests_results
- def _aggregate(self, tests_results: List[Dict]):
+ def _aggregate(self, tests_results: List[TestCaseSummary]) -> TestSuiteSummary:
""" aggregate multiple testcase results
Args:
tests_results (list): list of testcase summary
"""
- summary = {
+ testsuite_summary = {
"success": True,
"stat": {
"testcases": {
@@ -151,21 +149,21 @@ class HttpRunner(object):
}
for testcase_summary in tests_results:
- if testcase_summary["success"]:
- summary["stat"]["testcases"]["success"] += 1
+ if testcase_summary.success:
+ testsuite_summary["stat"]["testcases"]["success"] += 1
else:
- summary["stat"]["testcases"]["fail"] += 1
+ testsuite_summary["stat"]["testcases"]["fail"] += 1
- summary["success"] &= testcase_summary["success"]
+ testsuite_summary["success"] &= testcase_summary.success
- report.aggregate_stat(summary["stat"]["teststeps"], testcase_summary["stat"])
- report.aggregate_stat(summary["time"], testcase_summary["time"])
+ report.aggregate_stat(testsuite_summary["stat"]["teststeps"], testcase_summary.stat.dict())
+ report.aggregate_stat(testsuite_summary["time"], testcase_summary.time.dict())
- summary["details"].append(testcase_summary)
+ testsuite_summary["details"].append(testcase_summary)
- return summary
+ return TestSuiteSummary.parse_obj(testsuite_summary)
- def run_tests(self, tests_mapping):
+ def run_tests(self, tests_mapping) -> TestSuiteSummary:
""" run testcase/testsuite data
"""
tests = TestsMapping.parse_obj(tests_mapping)
@@ -195,7 +193,7 @@ class HttpRunner(object):
if self.save_tests:
utils.dump_json_file(
- self._summary,
+ self._summary.dict(),
utils.prepare_log_file_abs_path(self.test_path, "summary.json")
)
# save variables and export data
@@ -207,7 +205,7 @@ class HttpRunner(object):
return self._summary
- def run_path(self, path, dot_env_path=None, mapping=None):
+ def run_path(self, path, dot_env_path=None, mapping=None) -> TestSuiteSummary:
""" run testcase/testsuite file or folder.
Args:
diff --git a/httprunner/v3/response.py b/httprunner/v3/response.py
index 57f47eae..01af0bd8 100644
--- a/httprunner/v3/response.py
+++ b/httprunner/v3/response.py
@@ -25,7 +25,7 @@ class ResponseObject(object):
"headers": resp_obj.headers,
"body": resp_obj.json()
}
- self.validation_results = {}
+ self.validation_results: Dict = {}
def __getattr__(self, key):
try:
diff --git a/httprunner/v3/runner.py b/httprunner/v3/runner.py
index 4de890d8..b426b2ea 100644
--- a/httprunner/v3/runner.py
+++ b/httprunner/v3/runner.py
@@ -1,4 +1,4 @@
-from typing import List
+from typing import List, Dict
from loguru import logger
@@ -7,7 +7,7 @@ from httprunner.client import HttpSession
from httprunner.exceptions import ValidationFailure
from httprunner.v3.parser import build_url, parse_data, parse_variables_mapping
from httprunner.v3.response import ResponseObject
-from httprunner.v3.schema import TestsConfig, TestStep, VariablesMapping, TestCase
+from httprunner.v3.schema import TestsConfig, TestStep, VariablesMapping, TestCase, MetaData
class TestCaseRunner(object):
@@ -15,8 +15,8 @@ class TestCaseRunner(object):
config: TestsConfig = {}
teststeps: List[TestStep] = []
session: HttpSession = None
- meta_datas: List = []
- validation_results: List = []
+ meta_datas: List[MetaData] = []
+ validation_results: Dict = {}
def init(self, testcase: TestCase) -> "TestCaseRunner":
self.config = testcase.config
@@ -92,8 +92,8 @@ class TestCaseRunner(object):
finally:
self.validation_results = resp_obj.validation_results
# save request & response meta data
- self.session.meta_data["validators"] = self.validation_results
- self.session.meta_data["name"] = step.name
+ self.session.meta_data.validators = self.validation_results
+ self.session.meta_data.name = step.name
self.meta_datas.append(self.session.meta_data)
return extract_mapping
diff --git a/httprunner/v3/schema.py b/httprunner/v3/schema.py
index 41721a60..de2f144c 100644
--- a/httprunner/v3/schema.py
+++ b/httprunner/v3/schema.py
@@ -80,3 +80,74 @@ class ProjectMeta(BaseModel):
class TestsMapping(BaseModel):
project_mapping: ProjectMeta # TODO: rename to project_meta
testcases: List[TestCase]
+
+
+class Stat(BaseModel):
+ testcases: Dict
+ teststeps: Dict
+
+
+class TestCaseTime(BaseModel):
+ start_at: float
+ duration: float
+ start_datetime: Text = ""
+
+
+class TestCaseStat(BaseModel):
+ total: int = 0
+ successes: int = 0
+ failures: int = 0
+ errors: int = 0
+ skipped: int = 0
+ expectedFailures: int = 0
+ unexpectedSuccesses: int = 0
+
+
+class TestCaseInOut(BaseModel):
+ vars: VariablesMapping = {}
+ out: Export = []
+
+
+class RequestStat(BaseModel):
+ content_size: Text = "N/A"
+ response_time_ms: Text = "N/A"
+ elapsed_ms: Text = "N/A"
+
+
+class MetaData(BaseModel):
+ name: Text = ""
+ data: List[Dict]
+ stat: RequestStat
+ validators: Dict = {}
+
+
+class Record(BaseModel):
+ name: Text = ""
+ status: Text = ""
+ attachment: Text = ""
+ meta_datas: List[MetaData] = []
+ response_time: Text = "N/A"
+
+
+class TestCaseSummary(BaseModel):
+ name: Text = ""
+ success: bool
+ stat: TestCaseStat
+ time: TestCaseTime
+ records: List = [Record]
+ in_out: TestCaseInOut = {}
+ log: Text = ""
+
+
+class PlatformInfo(BaseModel):
+ httprunner_version: Text
+ python_version: Text
+ platform: Text
+
+
+class TestSuiteSummary(BaseModel):
+ success: bool
+ stat: Stat
+ time: TestCaseTime
+ platform: PlatformInfo
+ details: List[TestCaseSummary]