diff --git a/httprunner/cli.py b/httprunner/cli.py
index 2270acf4..3ef74b97 100644
--- a/httprunner/cli.py
+++ b/httprunner/cli.py
@@ -9,8 +9,6 @@ from httprunner import __version__ as hrun_version
from httprunner import logger
from httprunner.task import HttpRunner
from httprunner.utils import create_scaffold, print_output, string_type
-from pyunitreport import __version__ as pyu_version
-from pyunitreport import HTMLTestRunner
def main_hrun():
@@ -24,6 +22,12 @@ def main_hrun():
parser.add_argument(
'testset_paths', nargs='*',
help="testset file path")
+ parser.add_argument(
+ '--html-report-name',
+ help="specify html report name, only effective when generating html report.")
+ parser.add_argument(
+ '--html-report-template',
+ help="specify html report template path.")
parser.add_argument(
'--log-level', default='INFO',
help="Specify logging level, default is INFO.")
@@ -38,8 +42,7 @@ def main_hrun():
logger.setup_logger(args.log_level)
if args.version:
- logger.color_print("HttpRunner version: {}".format(hrun_version), "GREEN")
- logger.color_print("PyUnitReport version: {}".format(pyu_version), "GREEN")
+ logger.color_print("{}".format(hrun_version), "GREEN")
exit(0)
project_name = args.startproject
@@ -49,14 +52,16 @@ def main_hrun():
exit(0)
kwargs = {
- "output": os.path.join(os.getcwd(), "reports"),
"failfast": args.failfast
}
- test_runner = HTMLTestRunner(**kwargs)
- result = HttpRunner(args.testset_paths, test_runner).run()
- print_output(result.output)
+ run_kwargs = {
+ "html_report_name": args.html_report_name,
+ "html_report_template": args.html_report_template
+ }
+ result = HttpRunner(args.testset_paths, **kwargs).run(**run_kwargs)
- return 0 if result.success else 1
+ print_output(result["output"])
+ return 0 if result["success"] else 1
def main_locust():
""" Performance test with locust: parse command line options and run commands.
diff --git a/httprunner/client.py b/httprunner/client.py
index 27d38e0e..5d150592 100644
--- a/httprunner/client.py
+++ b/httprunner/client.py
@@ -119,7 +119,6 @@ class HttpSession(requests.Session):
# prepend url with hostname unless it's already an absolute URL
url = self._build_url(url)
- print("")
logger.log_info("{method} {url}".format(method=method, url=url))
logger.log_debug("request kwargs(raw): {kwargs}".format(kwargs=kwargs))
# store meta data that is used when reporting the request to locust's statistics
diff --git a/httprunner/report.py b/httprunner/report.py
new file mode 100644
index 00000000..18efea1f
--- /dev/null
+++ b/httprunner/report.py
@@ -0,0 +1,150 @@
+import os
+import time
+import unittest
+from datetime import datetime
+
+from httprunner import logger
+from jinja2 import Template
+
+
+def get_summary(result):
+ """ get summary from test result
+ """
+ summary = {
+ "success": result.wasSuccessful(),
+ "stat": {
+ 'testsRun': 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"]["testsRun"] \
+ - summary["stat"]["failures"] \
+ - summary["stat"]["errors"] \
+ - summary["stat"]["skipped"] \
+ - summary["stat"]["expectedFailures"] \
+ - summary["stat"]["unexpectedSuccesses"]
+
+ if getattr(result, "records", None):
+ summary["time"] = {
+ 'start_at': datetime.fromtimestamp(result.start_at),
+ 'duration': result.duration
+ }
+ summary["records"] = result.records
+
+ return summary
+
+
+class HtmlTestResult(unittest.TextTestResult):
+ """A html result class that can generate formatted html results.
+
+ Used by TextTestRunner.
+ """
+ def __init__(self, stream, descriptions, verbosity):
+ super(HtmlTestResult, self).__init__(stream, descriptions, verbosity)
+ self.records = []
+ self.default_report_template_path = os.path.join(
+ os.path.abspath(os.path.dirname(__file__)),
+ "templates",
+ "default_report_template.html"
+ )
+ self.report_path = None
+
+ def _record_test(self, test, result_type, attachment=''):
+ self.records.append({
+ 'name': test.shortDescription(),
+ 'result_type': result_type,
+ 'start_at': datetime.fromtimestamp(test.start_at),
+ 'duration': time.time() - test.start_at,
+ 'attachment': attachment
+ })
+
+ def startTestRun(self):
+ self.start_at = time.time()
+
+ def startTest(self, test):
+ """ add start test time """
+ test.start_at = time.time()
+ super(HtmlTestResult, self).startTest(test)
+ logger.color_print(test.shortDescription(), "yellow")
+
+ def addSuccess(self, test):
+ super(HtmlTestResult, self).addSuccess(test)
+ self._record_test(test, 'success')
+ print("")
+
+ def addError(self, test, err):
+ super(HtmlTestResult, self).addError(test, err)
+ self._record_test(test, 'error', self._exc_info_to_string(err, test))
+ print("")
+
+ def addFailure(self, test, err):
+ super(HtmlTestResult, self).addFailure(test, err)
+ self._record_test(test, 'failure', self._exc_info_to_string(err, test))
+ print("")
+
+ def addSkip(self, test, reason):
+ super(HtmlTestResult, self).addSkip(test, reason)
+ self._record_test(test, 'skipped', reason)
+ print("")
+
+ def addExpectedFailure(self, test, err):
+ super(HtmlTestResult, self).addExpectedFailure(test, err)
+ self._record_test(test, 'ExpectedFailure', self._exc_info_to_string(err, test))
+ print("")
+
+ def addUnexpectedSuccess(self, test):
+ super(HtmlTestResult, self).addUnexpectedSuccess(test)
+ self._record_test(test, 'UnexpectedSuccess')
+ print("")
+
+ @property
+ def duration(self):
+ return time.time() - self.start_at
+
+ @property
+ def summary(self):
+ return get_summary(self)
+
+ def render_html_report(self, html_report_name=None, html_report_template=None):
+ """ render html report with specified report name and template
+ if html_report_name is not specified, use current datetime
+ if html_report_template is not specified, use default report template
+ """
+ if not html_report_template:
+ html_report_template = self.default_report_template_path
+ logger.log_debug("No html report template specified, use default.")
+ else:
+ logger.log_info("render with html report template: {}".format(html_report_template))
+
+ with open(html_report_template, "r") as fp:
+ template_content = fp.read()
+
+ summary = self.summary
+ logger.log_info("Start to render Html report ...")
+ logger.log_debug("render data: {}".format(summary))
+
+ report_dir_path = os.path.join(os.getcwd(), "reports")
+ start_datetime = summary["time"]["start_at"].strftime('%Y-%m-%d-%H-%M-%S')
+ if html_report_name:
+ summary["html_report_name"] = html_report_name
+ report_dir_path = os.path.join(report_dir_path, html_report_name)
+ html_report_name += "-{}.html".format(start_datetime)
+ else:
+ summary["html_report_name"] = ""
+ html_report_name = "{}.html".format(start_datetime)
+
+ if not os.path.isdir(report_dir_path):
+ os.makedirs(report_dir_path)
+
+ report_path = os.path.join(report_dir_path, html_report_name)
+ with open(report_path, 'w', encoding='utf-8') as fp:
+ rendered_content = Template(template_content).render(summary)
+ fp.write(rendered_content)
+
+ logger.log_info("Generated Html report: {}".format(report_path))
+
+ return report_path
diff --git a/httprunner/requirements-dev.txt b/httprunner/requirements-dev.txt
deleted file mode 100644
index 24a07fae..00000000
--- a/httprunner/requirements-dev.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-requests
-PyYAML
-PyUnitReport
-har2case
-colorama
-colorlog
-flask
\ No newline at end of file
diff --git a/httprunner/task.py b/httprunner/task.py
index 433b6191..8bd9f597 100644
--- a/httprunner/task.py
+++ b/httprunner/task.py
@@ -2,6 +2,7 @@ import sys
import unittest
from httprunner import exception, logger, runner, testcase, utils
+from httprunner.report import HtmlTestResult, get_summary
class TestCase(unittest.TestCase):
@@ -73,11 +74,10 @@ class TestSuite(unittest.TestSuite):
def _add_tests_to_suite(self, testcases):
for testcase_dict in testcases:
testcase_name = self.test_runner.context.eval_content(testcase_dict["name"])
- testcase_name_with_color = logger.coloring(testcase_name, "yellow")
if utils.PYTHON_VERSION == 3:
- TestCase.runTest.__doc__ = testcase_name_with_color
+ TestCase.runTest.__doc__ = testcase_name
else:
- TestCase.runTest.__func__.__doc__ = testcase_name_with_color
+ TestCase.runTest.__func__.__doc__ = testcase_name
test = TestCase(self.test_runner, testcase_dict)
[self.addTest(test) for _ in range(int(testcase_dict.get("times", 1)))]
@@ -126,61 +126,55 @@ class TaskSuite(unittest.TestSuite):
return self.suite_list
-class Result(object):
-
- class Stat(object):
- def __init__(self, **stat_dict):
- for key, value in stat_dict.items():
- setattr(self, key, value)
-
- def __init__(self, result, output):
- self.success = result.wasSuccessful()
- self.stat = self.make_stat(result)
- self.output = output
-
- def make_stat(self, result):
- total = result.testsRun
- failures = len(result.failures)
- errors = len(result.errors)
- skipped = len(result.skipped)
- successes = total - failures - errors - skipped
- stat = {
- "total": total,
- "successes": successes,
- "failures": failures,
- "errors": errors,
- "skipped": skipped
- }
- return self.Stat(**stat)
-
-
class HttpRunner(object):
- def __init__(self, path, runner=None):
+ def __init__(self, path, **kwargs):
""" initialize HttpRunner with specified testset file path and test runner
@params:
- path: YAML/JSON testset file path
- - runner: HTMLTestRunner() or TextTestRunner()
+ - gen_html_report: True/False
+ - failfast: False/True, stop the test run on the first error or failure.
"""
self.path = path
- self.runner = runner or unittest.TextTestRunner()
- def run(self, mapping=None):
+ self.gen_html_report = kwargs.pop("gen_html_report", True)
+ if self.gen_html_report:
+ kwargs["resultclass"] = HtmlTestResult
+
+ self.runner = unittest.TextTestRunner(**kwargs)
+
+ def run(self, **kwargs):
""" start to run suite
+ @param mapping
if mapping specified, it will override variables in config block
+ @param html_report_name
+ output html report file name
+ @param html_report_template
+ report template file path, template should be in Jinja2 format
"""
try:
- mapping = mapping or {}
+ mapping = kwargs.get("mapping", {})
task_suite = TaskSuite(self.path, mapping)
except exception.TestcaseNotFound:
sys.exit(1)
result = self.runner.run(task_suite)
+
output = {}
for task in task_suite.tasks:
output.update(task.output)
- return Result(result, output)
+ if self.gen_html_report:
+ summary = result.summary
+ summary["report_path"] = result.render_html_report(
+ kwargs.get("html_report_name"),
+ kwargs.get("html_report_template")
+ )
+ else:
+ summary = get_summary(result)
+
+ summary["output"] = output
+ return summary
class LocustTask(object):
diff --git a/httprunner/templates/default_report_template.html b/httprunner/templates/default_report_template.html
new file mode 100644
index 00000000..d1c9b650
--- /dev/null
+++ b/httprunner/templates/default_report_template.html
@@ -0,0 +1,102 @@
+
+
+
+ {{html_report_name}} - TestReport
+
+
+
+
+ Test Report: {{html_report_name}}
+
+ Summary
+
+
+
+ | START AT |
+ {{time.start_at.strftime('%Y-%m-%d %H:%M:%S')}} |
+
+
+ | DURATION |
+ {{ '%0.3f'| format(time.duration|float) }} seconds |
+
+
+ | TOTAL |
+ SUCCESS |
+ FAILED |
+ ERROR |
+ SKIPPED |
+
+
+
+ | {{stat.testsRun}} |
+ {{stat.successes}} |
+ {{stat.failures}} |
+ {{stat.errors}} |
+ {{stat.skipped}} |
+
+
+
+
+ Details
+
+
+ | Status |
+ Name |
+ Duration |
+ Info |
+
+ {% for record in records %}
+
+ | {{record.result_type}}
+ | {{record.name}} |
+ {{ '%0.3f'| format(record.duration|float) }} seconds |
+ {{record.attachment}} |
+
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 24a07fae..92215a39 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,6 +1,6 @@
requests
PyYAML
-PyUnitReport
+Jinja2
har2case
colorama
colorlog
diff --git a/setup.py b/setup.py
index 05dabe72..0bb14e3e 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@ with io.open("README.rst", encoding='utf-8') as f:
install_requires = [
"requests",
"PyYAML",
- "PyUnitReport",
+ "Jinja2",
"har2case",
"colorama",
"colorlog"
@@ -27,7 +27,7 @@ setup(
license='MIT',
packages=find_packages(exclude=["examples", "tests", "tests.*"]),
package_data={
- 'httprunner': ['locustfile_template'],
+ 'httprunner': ['locustfile_template', "templates/default_report_template.html"],
},
keywords='api test',
install_requires=install_requires,
diff --git a/tests/test_cli.py b/tests/test_cli.py
deleted file mode 100644
index a9a7b902..00000000
--- a/tests/test_cli.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import os
-import shutil
-import sys
-
-from httprunner.task import TaskSuite
-from pyunitreport import HTMLTestRunner
-from tests.base import ApiServerUnittest
-
-
-class TestCli(ApiServerUnittest):
-
- def setUp(self):
- testset_path = "tests/data/demo_testset_cli.yml"
- output_folder_name = os.path.basename(os.path.splitext(testset_path)[0])
- self.kwargs = {
- "output": output_folder_name
- }
- self.task_suite = TaskSuite(testset_path)
- self.report_save_dir = os.path.join(os.getcwd(), 'reports', output_folder_name)
- self.reset_all()
-
- def reset_all(self):
- url = "%s/api/reset-all" % self.host
- headers = self.get_authenticated_headers()
- return self.api_client.get(url, headers=headers)
-
- def test_run_times(self):
- result = HTMLTestRunner(**self.kwargs).run(self.task_suite)
- self.assertEqual(result.testsRun, 10)
- shutil.rmtree(self.report_save_dir)
-
- def test_skip(self):
- result = HTMLTestRunner(**self.kwargs).run(self.task_suite)
- self.assertEqual(len(result.skipped), 4)
- shutil.rmtree(self.report_save_dir)
diff --git a/tests/test_httprunner.py b/tests/test_httprunner.py
new file mode 100644
index 00000000..2558745a
--- /dev/null
+++ b/tests/test_httprunner.py
@@ -0,0 +1,46 @@
+import os
+import shutil
+
+from httprunner import HttpRunner
+from tests.base import ApiServerUnittest
+
+
+class TestHttpRunner(ApiServerUnittest):
+
+ def setUp(self):
+ self.testset_path = "tests/data/demo_testset_cli.yml"
+ self.reset_all()
+
+ def reset_all(self):
+ url = "%s/api/reset-all" % self.host
+ headers = self.get_authenticated_headers()
+ return self.api_client.get(url, headers=headers)
+
+ def test_text_run_times(self):
+ kwargs = {
+ "gen_html_report": False
+ }
+ result = HttpRunner(self.testset_path, **kwargs).run()
+ self.assertEqual(result["stat"]["testsRun"], 10)
+
+ def test_text_skip(self):
+ kwargs = {
+ "gen_html_report": False
+ }
+ result = HttpRunner(self.testset_path, **kwargs).run()
+ self.assertEqual(result["stat"]["skipped"], 4)
+
+ def test_html_report(self):
+ kwargs = {
+ "gen_html_report": True
+ }
+ output_folder_name = os.path.basename(os.path.splitext(self.testset_path)[0])
+ run_kwargs = {
+ "html_report_name": output_folder_name
+ }
+ result = HttpRunner(self.testset_path).run(**run_kwargs)
+ self.assertEqual(result["stat"]["testsRun"], 10)
+ self.assertEqual(result["stat"]["skipped"], 4)
+
+ report_save_dir = os.path.join(os.getcwd(), 'reports', output_folder_name)
+ shutil.rmtree(report_save_dir)
diff --git a/tests/test_runner.py b/tests/test_runner.py
index 017bbee1..4016448e 100644
--- a/tests/test_runner.py
+++ b/tests/test_runner.py
@@ -78,50 +78,50 @@ class TestRunner(ApiServerUnittest):
def test_run_testset_hardcode(self):
for testcase_file_path in self.testcase_file_path_list:
result = HttpRunner(testcase_file_path).run()
- self.assertTrue(result.success)
+ self.assertTrue(result["success"])
def test_run_testsets_hardcode(self):
result = HttpRunner(self.testcase_file_path_list).run()
- self.assertTrue(result.success)
- self.assertEqual(result.stat.total, 6)
- self.assertEqual(result.stat.successes, 6)
+ self.assertTrue(result["success"])
+ self.assertEqual(result["stat"]["testsRun"], 6)
+ self.assertEqual(result["stat"]["successes"], 6)
def test_run_testset_template_variables(self):
testcase_file_path = os.path.join(
os.getcwd(), 'tests/data/demo_testset_variables.yml')
result = HttpRunner(testcase_file_path).run()
- self.assertTrue(result.success)
+ self.assertTrue(result["success"])
def test_run_testset_template_import_functions(self):
testcase_file_path = os.path.join(
os.getcwd(), 'tests/data/demo_testset_template_import_functions.yml')
result = HttpRunner(testcase_file_path).run()
- self.assertTrue(result.success)
+ self.assertTrue(result["success"])
def test_run_testsets_template_import_functions(self):
testcase_file_path = os.path.join(
os.getcwd(), 'tests/data/demo_testset_template_import_functions.yml')
result = HttpRunner(testcase_file_path).run()
- self.assertTrue(result.success)
+ self.assertTrue(result["success"])
def test_run_testsets_template_lambda_functions(self):
testcase_file_path = os.path.join(
os.getcwd(), 'tests/data/demo_testset_template_lambda_functions.yml')
result = HttpRunner(testcase_file_path).run()
- self.assertTrue(result.success)
+ self.assertTrue(result["success"])
def test_run_testset_layered(self):
testcase_file_path = os.path.join(
os.getcwd(), 'tests/data/demo_testset_layer.yml')
result = HttpRunner(testcase_file_path).run()
- self.assertTrue(result.success)
+ self.assertTrue(result["success"])
def test_run_testset_output(self):
testcase_file_path = os.path.join(
os.getcwd(), 'tests/data/demo_testset_layer.yml')
result = HttpRunner(testcase_file_path).run()
- self.assertTrue(result.success)
- self.assertIn("token", result.output)
+ self.assertTrue(result["success"])
+ self.assertIn("token", result["output"])
def test_run_testset_with_variables_mapping(self):
testcase_file_path = os.path.join(
@@ -129,9 +129,9 @@ class TestRunner(ApiServerUnittest):
variables_mapping = {
"app_version": '2.9.7'
}
- result = HttpRunner(testcase_file_path).run(variables_mapping)
- self.assertTrue(result.success)
- self.assertIn("token", result.output)
+ result = HttpRunner(testcase_file_path).run(mapping=variables_mapping)
+ self.assertTrue(result["success"])
+ self.assertIn("token", result["output"])
def test_run_testcase_with_empty_header(self):
testcase_file_path = os.path.join(
@@ -162,6 +162,6 @@ class TestRunner(ApiServerUnittest):
testcase_file_path = os.path.join(
os.getcwd(), 'tests/data/demo_parameters.yml')
result = HttpRunner(testcase_file_path).run()
- self.assertTrue(result.success)
- self.assertIn("token", result.output)
- self.assertEqual(result.stat.total, 6)
+ self.assertTrue(result["success"])
+ self.assertIn("token", result["output"])
+ self.assertEqual(result["stat"]["testsRun"], 6)