From 160218b1edcdcccd75c2b07212ef2c77d14e0562 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 25 Feb 2018 23:09:12 +0800 Subject: [PATCH] 0.9.5: 1, remove PyUnitReport dependency; 2, add built-in html report; 3, support pass in html report template, in Jinja2 format. --- httprunner/cli.py | 23 +-- httprunner/client.py | 1 - httprunner/report.py | 150 ++++++++++++++++++ httprunner/requirements-dev.txt | 7 - httprunner/task.py | 68 ++++---- .../templates/default_report_template.html | 102 ++++++++++++ requirements-dev.txt | 2 +- setup.py | 4 +- tests/test_cli.py | 35 ---- tests/test_httprunner.py | 46 ++++++ tests/test_runner.py | 34 ++-- 11 files changed, 363 insertions(+), 109 deletions(-) create mode 100644 httprunner/report.py delete mode 100644 httprunner/requirements-dev.txt create mode 100644 httprunner/templates/default_report_template.html delete mode 100644 tests/test_cli.py create mode 100644 tests/test_httprunner.py 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
TOTALSUCCESSFAILEDERRORSKIPPED
{{stat.testsRun}}{{stat.successes}}{{stat.failures}}{{stat.errors}}{{stat.skipped}}
+ +

Details

+ + + + + + + + {% for record in records %} + + + + + + {% endfor %} +
StatusNameDurationInfo
{{record.result_type}} + {{record.name}}{{ '%0.3f'| format(record.duration|float) }} seconds
{{record.attachment}}
+ \ 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)