├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── assets └── report.gif ├── lib ├── HTMLTestReportCN.py └── __init__.py ├── requirements.txt └── samples ├── RunAllTests.py ├── __init__.py └── testcases ├── __init__.py ├── test1.py └── test2.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | venv 3 | *.iml 4 | report 5 | src/report 6 | __pycache__ 7 | */__pycache__ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Gelomen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install freeze 2 | 3 | # 安装依赖 4 | install: 5 | pip3 install -r requirements.txt --break-system-packages 6 | 7 | # 锁定依赖 8 | freeze: 9 | pip3 list --format=freeze > requirements.txt 10 | 11 | # 启动测试 12 | run: 13 | python3 src/RunAllTests.py 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTMLTestReportCN-ScreenShot 2 | 3 | 带有截图功能的 `HTMLTestReportCN`, 基于 [findyou](https://github.com/findyou) 和 [boafantasy](https://github.com/boafantasy) 两位的版本修改的, 添加功能并修复bug和优化细节 4 | 5 | - 目前同时拥有无截图和有截图报告功能, 通过参数 `need_screenshot` 开启截图功能 6 | - 生成的报告有饼图显示, 测试结果比较直观 7 | - [findyou](https://github.com/findyou) 版本: https://github.com/findyou/HTMLTestRunnerCN 8 | - [boafantasy](https://github.com/boafantasy) 版本: https://github.com/boafantasy/HTMLTestRunnerCN 9 | 10 | ## 步骤 11 | 12 | 查看例子: [Samples](./samples) 13 | 14 | ### 1. 初始化修饰器 15 | 16 | 新建 `RunAllTests.py`, 如何封装可以自行决定 17 | 18 | - `run()` 添加 `@HTMLTestReportCN.init()` 修饰器, 并把报告目录和报告标题传入 19 | - 调用 `HTMLTestReportCN.get_report_path()` 获取 `HTML` 报告路径, 用于测试结果写入 20 | 21 | ```python 22 | import unittest 23 | from lib import HTMLTestReportCN 24 | 25 | report_dir = "./report" 26 | title = "自动化测试报告" 27 | description = "测试报告" 28 | test_case_path = "./testcases" 29 | 30 | 31 | @HTMLTestReportCN.init(report_dir=report_dir, title=title) 32 | def run(): 33 | test_suite = unittest.TestLoader().discover(test_case_path) 34 | 35 | fp = open(HTMLTestReportCN.get_report_path(), "wb") 36 | runner = HTMLTestReportCN.HTMLTestRunner( 37 | stream=fp, title=title, description=description, tester=input("请输入你的名字: ") 38 | ) 39 | runner.run(test_suite) 40 | fp.close() 41 | 42 | 43 | if __name__ == "__main__": 44 | run() 45 | 46 | ``` 47 | 48 | ### 2. 截图修饰器 49 | 50 | 在需要截图的测试用例添加 `@HTMLTestReportCN.screenshot` 修饰器, 失败时会自动截图 51 | 52 | ```python 53 | from lib import HTMLTestReportCN 54 | 55 | ... 56 | 57 | @HTMLTestReportCN.screenshot 58 | def test1_find_input(self): 59 | ... 60 | 61 | ``` 62 | 63 | ----- 64 | 65 | ## 效果预览 66 | 67 | ![效果预览](assets/report.gif) 68 | -------------------------------------------------------------------------------- /assets/report.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gelomen/HTMLTestReportCN-ScreenShot/4a6f5b314c528082b03162d685d0bf3dbfc9aed2/assets/report.gif -------------------------------------------------------------------------------- /lib/HTMLTestReportCN.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | A TestRunner for use with the Python unit testing framework. It 4 | generates a HTML report to show the result at a glance. 5 | 6 | The simplest way to use this is to invoke its main method. E.g. 7 | 8 | import unittest 9 | import HTMLTestRunner 10 | 11 | ... define your tests ... 12 | 13 | if __name__ == '__main__': 14 | HTMLTestRunner.main() 15 | 16 | 17 | For more customization options, instantiates a HTMLTestRunner object. 18 | HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. 19 | 20 | # output to a file 21 | fp = file('my_report.html', 'wb') 22 | runner = HTMLTestRunner.HTMLTestRunner( 23 | stream=fp, 24 | title='My unit test', 25 | description='This demonstrates the report output by HTMLTestRunner.' 26 | ) 27 | 28 | # Use an external stylesheet. 29 | # See the Template_mixin class for more customizable options 30 | runner.STYLESHEET_TMPL = '' 31 | 32 | # run the test 33 | runner.run(my_test_suite) 34 | 35 | 36 | ------------------------------------------------------------------------ 37 | Copyright (c) 2004-2007, Wai Yip Tung 38 | All rights reserved. 39 | 40 | Redistribution and use in source and binary forms, with or without 41 | modification, are permitted provided that the following conditions are 42 | met: 43 | 44 | * Redistributions of source code must retain the above copyright notice, 45 | this list of conditions and the following disclaimer. 46 | * Redistributions in binary form must reproduce the above copyright 47 | notice, this list of conditions and the following disclaimer in the 48 | documentation and/or other materials provided with the distribution. 49 | * Neither the name Wai Yip Tung nor the names of its contributors may be 50 | used to endorse or promote products derived from this software without 51 | specific prior written permission. 52 | 53 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 54 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 55 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 56 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 57 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 58 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 59 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 60 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 61 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 62 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 63 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 64 | """ 65 | 66 | # URL: http://tungwaiyip.info/software/HTMLTestRunner.html 67 | # URL: https://github.com/Gelomen/HTMLTestReportCN-ScreenShot 68 | 69 | __author__ = "Wai Yip Tung, Findyou, boafantasy, Gelomen" 70 | __version__ = "1.2.0" 71 | 72 | """ 73 | Version 0.8.2.1 -Findyou 74 | * 改为支持python3 75 | 76 | Version 0.8.2.1 -Findyou 77 | * 支持中文,汉化 78 | * 调整样式,美化(需要连入网络,使用的百度的Bootstrap.js) 79 | * 增加 通过分类显示、测试人员、通过率的展示 80 | * 优化“详细”与“收起”状态的变换 81 | * 增加返回顶部的锚点 82 | 83 | Version 0.8.2 84 | * Show output inline instead of popup window (Viorel Lupu). 85 | 86 | Version in 0.8.1 87 | * Validated XHTML (Wolfgang Borgert). 88 | * Added description of test classes and test cases. 89 | 90 | Version in 0.8.0 91 | * Define Template_mixin class for customization. 92 | * Workaround a IE 6 bug that it does not treat 246 | 247 | 248 | 249 | %(stylesheet)s 250 | 251 | 252 | 518 | %(heading)s 519 | %(report)s 520 | %(ending)s 521 | 522 | 523 | 524 | """ 525 | # variables: (title, generator, stylesheet, heading, report, ending) 526 | 527 | # ------------------------------------------------------------------------ 528 | # Stylesheet 529 | # 530 | # alternatively use a for external style sheet, e.g. 531 | # 532 | 533 | STYLESHEET_TMPL = """ 534 | 624 | """ 625 | 626 | # ------------------------------------------------------------------------ 627 | # Heading 628 | # 629 | 630 | # 添加显示截图 和 饼状图 的div -- Gelomen 631 | HEADING_TMPL = """
632 |
633 |
634 |

%(title)s

635 | %(parameters)s 636 |

%(description)s

637 |
638 |
639 |
640 | 641 | """ # variables: (title, parameters, description) 642 | 643 | HEADING_ATTRIBUTE_TMPL = """

%(name)s : %(value)s

644 | """ # variables: (name, value) 645 | 646 | # ------------------------------------------------------------------------ 647 | # Report 648 | # 649 | # 汉化,加美化效果 --Findyou 650 | REPORT_TMPL = """ 651 |
652 |

653 | 概要{ %(passrate)s } 654 | 通过{ %(Pass)s } 655 | 失败{ %(fail)s } 656 | 错误{ %(error)s } 657 | 所有{ %(count)s } 658 |

659 |
660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | %(test_list)s 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 |
用例集/测试用例说明总计通过失败错误耗时详细
总计%(count)s%(Pass)s%(fail)s%(error)s%(time_usage)s通过率:%(passrate)s
692 | """ # variables: (test_list, count, Pass, fail, error ,passrate) 693 | 694 | REPORT_CLASS_TMPL = r""" 695 | 696 | %(name)s 697 | %(doc)s 698 | %(count)s 699 | %(Pass)s 700 | %(fail)s 701 | %(error)s 702 | %(time_usage)s 703 | 详细 704 | 705 | """ # variables: (style, desc, count, Pass, fail, error, cid) 706 | 707 | # 失败 的样式,去掉原来JS效果,美化展示效果 -Findyou / 美化类名上下居中,有截图列 -- Gelomen 708 | REPORT_TEST_WITH_OUTPUT_TMPL_1 = r""" 709 | 710 |
%(name)s
711 | %(doc)s 712 | 713 | 716 | 717 | 718 | 719 |
720 |
 721 |     %(script)s
 722 |     
723 |
724 | 725 |
浏览器版本:
%(browser)s

截图:%(screenshot)s
726 | 727 | """ # variables: (tid, Class, style, desc, status) 728 | 729 | # 失败 的样式,去掉原来JS效果,美化展示效果 -Findyou / 美化类名上下居中,无截图列 -- Gelomen 730 | REPORT_TEST_WITH_OUTPUT_TMPL_0 = r""" 731 | 732 |
%(name)s
733 | %(doc)s 734 | 735 | 738 | 739 | 740 | 741 |
742 |
 743 |         %(script)s
 744 |         
745 |
746 | 747 | 748 | 749 | """ # variables: (tid, Class, style, desc, status) 750 | 751 | # 通过 的样式,加标签效果 -Findyou / 美化类名上下居中 -- Gelomen 752 | REPORT_TEST_NO_OUTPUT_TMPL = r""" 753 | 754 |
%(name)s
755 | %(doc)s 756 | %(status)s 757 | 758 | 759 | """ # variables: (tid, Class, style, desc, status) 760 | 761 | REPORT_TEST_OUTPUT_TMPL = r""" 762 | %(id)s: %(output)s 763 | """ # variables: (id, output) 764 | 765 | # ------------------------------------------------------------------------ 766 | # ENDING 767 | # 768 | # 增加返回顶部按钮 --Findyou 769 | ENDING_TMPL = """
 
770 | 773 | """ 774 | 775 | 776 | # -------------------- The end of the Template class ------------------- 777 | 778 | 779 | TestResult = unittest.TestResult 780 | 781 | 782 | class _TestResult(TestResult): 783 | # note: _TestResult is a pure representation of results. 784 | # It lacks the output and reporting ability compares to unittest._TextTestResult. 785 | 786 | def __init__(self, verbosity=1): 787 | TestResult.__init__(self) 788 | self.stdout0 = None 789 | self.stderr0 = None 790 | self.success_count = 0 791 | self.failure_count = 0 792 | self.error_count = 0 793 | self.verbosity = verbosity 794 | 795 | # result is a list of result in 4 tuple 796 | # ( 797 | # result code (0: success; 1: fail; 2: error), 798 | # TestCase object, 799 | # Test output (byte string), 800 | # stack trace, 801 | # ) 802 | self.result = [] 803 | # 增加一个测试通过率 --Findyou 804 | self.passrate = float(0) 805 | 806 | # 增加失败用例合集 807 | self.failCase = "" 808 | # 增加错误用例合集 809 | self.errorCase = "" 810 | 811 | def startTest(self, test): 812 | stream = sys.stderr 813 | # stdout_content = " Testing: " + str(test) 814 | # stream.write(stdout_content) 815 | # stream.flush() 816 | # stream.write("\n") 817 | TestResult.startTest(self, test) 818 | # just one buffer for both stdout and stderr 819 | self.outputBuffer = io.StringIO() 820 | stdout_redirector.fp = self.outputBuffer 821 | stderr_redirector.fp = self.outputBuffer 822 | self.stdout0 = sys.stdout 823 | self.stderr0 = sys.stderr 824 | sys.stdout = stdout_redirector 825 | sys.stderr = stderr_redirector 826 | self.test_start_time = round(time.time(), 2) 827 | 828 | def complete_output(self): 829 | """ 830 | Disconnect output redirection and return buffer. 831 | Safe to call multiple times. 832 | """ 833 | self.test_end_time = round(time.time(), 2) 834 | if self.stdout0: 835 | sys.stdout = self.stdout0 836 | sys.stderr = self.stderr0 837 | self.stdout0 = None 838 | self.stderr0 = None 839 | return self.outputBuffer.getvalue() 840 | 841 | def stopTest(self, test): 842 | # Usually one of addSuccess, addError or addFailure would have been called. 843 | # But there are some path in unittest that would bypass this. 844 | # We must disconnect stdout in stopTest(), which is guaranteed to be called. 845 | self.complete_output() 846 | 847 | def addSuccess(self, test): 848 | self.success_count += 1 849 | TestResult.addSuccess(self, test) 850 | output = self.complete_output() 851 | use_time = round(self.test_end_time - self.test_start_time, 2) 852 | self.result.append((0, test, output, '', use_time)) 853 | if self.verbosity > 1: 854 | sys.stderr.write(' S ') 855 | sys.stderr.write(str(test)) 856 | sys.stderr.write('\n') 857 | else: 858 | sys.stderr.write(' S ') 859 | sys.stderr.write('\n') 860 | 861 | def addError(self, test, err): 862 | self.error_count += 1 863 | TestResult.addError(self, test, err) 864 | _, _exc_str = self.errors[-1] 865 | output = self.complete_output() 866 | use_time = round(self.test_end_time - self.test_start_time, 2) 867 | self.result.append((2, test, output, _exc_str, use_time)) 868 | if self.verbosity > 1: 869 | sys.stderr.write(' E ') 870 | sys.stderr.write(str(test)) 871 | sys.stderr.write('\n') 872 | else: 873 | sys.stderr.write(' E ') 874 | sys.stderr.write('\n') 875 | 876 | # 添加收集错误用例名字 -- Gelomen 877 | self.errorCase += "
  • " + str(test) + "
  • " 878 | 879 | def addFailure(self, test, err): 880 | self.failure_count += 1 881 | TestResult.addFailure(self, test, err) 882 | _, _exc_str = self.failures[-1] 883 | output = self.complete_output() 884 | use_time = round(self.test_end_time - self.test_start_time, 2) 885 | self.result.append((1, test, output, _exc_str, use_time)) 886 | if self.verbosity > 1: 887 | sys.stderr.write(' F ') 888 | sys.stderr.write(str(test)) 889 | sys.stderr.write('\n') 890 | else: 891 | sys.stderr.write(' F ') 892 | sys.stderr.write('\n') 893 | 894 | # 添加收集失败用例名字 -- Gelomen 895 | self.failCase += "
  • " + str(test) + "
  • " 896 | 897 | 898 | # 新增 need_screenshot 参数,-1为无需截图,否则需要截图 -- Gelomen 899 | class HTMLTestRunner(Template_mixin): 900 | """ 901 | """ 902 | 903 | def __init__(self, stream=sys.stdout, verbosity=2, title=None, description=None, tester=None): 904 | self.need_screenshot = 0 905 | self.stream = stream 906 | self.verbosity = verbosity 907 | if title is None: 908 | self.title = self.DEFAULT_TITLE 909 | else: 910 | self.title = title 911 | if description is None: 912 | self.description = self.DEFAULT_DESCRIPTION 913 | else: 914 | self.description = description 915 | if tester is None: 916 | self.tester = self.DEFAULT_TESTER 917 | else: 918 | self.tester = tester 919 | 920 | self.startTime = datetime.datetime.now() 921 | 922 | def run(self, test): 923 | "Run the given test case or test suite." 924 | result = _TestResult(self.verbosity) # verbosity为1,只输出成功与否,为2会输出用例名称 925 | test(result) 926 | self.stopTime = datetime.datetime.now() 927 | self.generateReport(test, result) 928 | # 优化测试结束后打印蓝色提示文字 -- Gelomen 929 | print("\n\033[36;0m--------------------- 测试结束 ---------------------\n" 930 | "------------- 合计耗时: %s -------------\033[0m" % (self.stopTime - self.startTime), file=sys.stderr) 931 | return result 932 | 933 | def sortResult(self, result_list): 934 | # unittest does not seems to run in any particular order. 935 | # Here at least we want to group them together by class. 936 | rmap = {} 937 | classes = [] 938 | for n, t, o, e, s in result_list: 939 | cls = t.__class__ 940 | if cls not in rmap: 941 | rmap[cls] = [] 942 | classes.append(cls) 943 | rmap[cls].append((n, t, o, e, s)) 944 | r = [(cls, rmap[cls]) for cls in classes] 945 | return r 946 | 947 | # 替换测试结果status为通过率 --Findyou 948 | def getReportAttributes(self, result): 949 | """ 950 | Return report attributes as a list of (name, value). 951 | Override this to add custom attributes. 952 | """ 953 | startTime = str(self.startTime)[:19] 954 | duration = str(self.stopTime - self.startTime) 955 | status = [] 956 | status.append('共 %s' % (result.success_count + result.failure_count + result.error_count)) 957 | if result.success_count: 958 | status.append('通过 %s' % result.success_count) 959 | if result.failure_count: 960 | status.append('失败 %s' % result.failure_count) 961 | if result.error_count: 962 | status.append('错误 %s' % result.error_count) 963 | if status: 964 | status = ','.join(status) 965 | if (result.success_count + result.failure_count + result.error_count) > 0: 966 | self.passrate = str("%.2f%%" % (float(result.success_count) / float( 967 | result.success_count + result.failure_count + result.error_count) * 100)) 968 | else: 969 | self.passrate = "0.00 %" 970 | else: 971 | status = 'none' 972 | 973 | if len(result.failCase) > 0: 974 | failCase = result.failCase 975 | else: 976 | failCase = "无" 977 | 978 | if len(result.errorCase) > 0: 979 | errorCase = result.errorCase 980 | else: 981 | errorCase = "无" 982 | 983 | return [ 984 | ('测试人员', self.tester), 985 | ('开始时间', startTime), 986 | ('合计耗时', duration), 987 | ('测试结果', status + ",通过率 = " + self.passrate), 988 | ('失败用例合集', failCase), 989 | ('错误用例合集', errorCase), 990 | ] 991 | 992 | def generateReport(self, test, result): 993 | report_attrs = self.getReportAttributes(result) 994 | generator = 'HTMLTestRunner %s' % __version__ 995 | stylesheet = self._generate_stylesheet() 996 | # 添加 通过、失败 和 错误 的统计,以用于饼图 -- Gelomen 997 | Pass = self._generate_report(result)["Pass"] 998 | fail = self._generate_report(result)["fail"] 999 | error = self._generate_report(result)["error"] 1000 | 1001 | heading = self._generate_heading(report_attrs) 1002 | report = self._generate_report(result)["report"] 1003 | ending = self._generate_ending() 1004 | output = self.HTML_TMPL % dict( 1005 | title=saxutils.escape(self.title), 1006 | generator=generator, 1007 | stylesheet=stylesheet, 1008 | Pass=Pass, 1009 | fail=fail, 1010 | error=error, 1011 | heading=heading, 1012 | report=report, 1013 | ending=ending, 1014 | ) 1015 | self.stream.write(output.encode('utf8')) 1016 | 1017 | def _generate_stylesheet(self): 1018 | return self.STYLESHEET_TMPL 1019 | 1020 | # 增加Tester显示 -Findyou 1021 | # 增加 失败用例合集 和 错误用例合集 的显示 -- Gelomen 1022 | def _generate_heading(self, report_attrs): 1023 | a_lines = [] 1024 | for name, value in report_attrs: 1025 | # 如果是 失败用例 或 错误用例合集,则不进行转义 -- Gelomen 1026 | if name == "失败用例合集": 1027 | if value == "无": 1028 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1029 | name=name, 1030 | value="
      " + value + "
    ", 1031 | ) 1032 | else: 1033 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1034 | name=name, 1035 | value="
    点击查看
    " 1036 | "
      " + value + "
    ", 1037 | ) 1038 | elif name == "错误用例合集": 1039 | if value == "无": 1040 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1041 | name=name, 1042 | value="
      " + value + "
    ", 1043 | ) 1044 | else: 1045 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1046 | name=name, 1047 | value="
    点击查看
    " 1048 | "
      " + value + "
    ", 1049 | ) 1050 | else: 1051 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1052 | name=saxutils.escape(name), 1053 | value=saxutils.escape(value), 1054 | ) 1055 | a_lines.append(line) 1056 | heading = self.HEADING_TMPL % dict( 1057 | title=saxutils.escape(self.title), 1058 | parameters=''.join(a_lines), 1059 | description=saxutils.escape(self.description), 1060 | tester=saxutils.escape(self.tester), 1061 | ) 1062 | return heading 1063 | 1064 | # 生成报告 --Findyou添加注释 1065 | def _generate_report(self, result): 1066 | rows = [] 1067 | sortedResult = self.sortResult(result.result) 1068 | # 所有用例统计耗时初始化 1069 | sum_ns = 0 1070 | for cid, (cls, cls_results) in enumerate(sortedResult): 1071 | # subtotal for a class 1072 | np = nf = ne = ns = 0 1073 | for n, t, o, e, s in cls_results: 1074 | if n == 0: 1075 | np += 1 1076 | elif n == 1: 1077 | nf += 1 1078 | elif n == 2: 1079 | ne += 1 1080 | ns += s # 把单个class用例文件里面的多个def用例每次的耗时相加 1081 | ns = round(ns, 2) 1082 | sum_ns += ns # 把所有用例的每次耗时相加 1083 | # format class description 1084 | # if cls.__module__ == "__main__": 1085 | # name = cls.__name__ 1086 | # else: 1087 | # name = "%s.%s" % (cls.__module__, cls.__name__) 1088 | name = cls.__name__ 1089 | doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 1090 | # desc = doc and '%s - %s' % (name, doc) or name 1091 | 1092 | row = self.REPORT_CLASS_TMPL % dict( 1093 | style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 1094 | name=name, 1095 | doc=doc, 1096 | count=np + nf + ne, 1097 | Pass=np, 1098 | fail=nf, 1099 | error=ne, 1100 | cid='c%s' % (cid + 1), 1101 | time_usage=str(ns) + "秒" # 单个用例耗时 1102 | ) 1103 | rows.append(row) 1104 | 1105 | for tid, (n, t, o, e, s) in enumerate(cls_results): 1106 | self._generate_report_test(rows, cid, tid, n, t, o, e) 1107 | sum_ns = round(sum_ns, 2) 1108 | report = self.REPORT_TMPL % dict( 1109 | test_list=''.join(rows), 1110 | count=str(result.success_count + result.failure_count + result.error_count), 1111 | Pass=str(result.success_count), 1112 | fail=str(result.failure_count), 1113 | error=str(result.error_count), 1114 | time_usage=str(sum_ns) + "秒", # 所有用例耗时 1115 | passrate=self.passrate, 1116 | ) 1117 | 1118 | # 获取 通过、失败 和 错误 的统计并return,以用于饼图 -- Gelomen 1119 | Pass = str(result.success_count) 1120 | fail = str(result.failure_count) 1121 | error = str(result.error_count) 1122 | return {"report": report, "Pass": Pass, "fail": fail, "error": error} 1123 | 1124 | def _generate_report_test(self, rows, cid, tid, n, t, o, e): 1125 | # e.g. 'pt1_1', 'ft1_1', 'et1_1'etc 1126 | has_output = bool(o or e) 1127 | # ID修改点为下划线,支持Bootstrap折叠展开特效 - Findyou 1128 | if n == 0: 1129 | tid_flag = 'p' 1130 | elif n == 1: 1131 | tid_flag = 'f' 1132 | elif n == 2: 1133 | tid_flag = 'e' 1134 | tid = tid_flag + 't%s_%s' % (cid + 1, tid + 1) 1135 | name = t.id().split('.')[-1] 1136 | doc = t.shortDescription() or "" 1137 | # desc = doc and ('%s - %s' % (name, doc)) or name 1138 | 1139 | # utf-8 支持中文 - Findyou 1140 | # o and e should be byte string because they are collected from stdout and stderr? 1141 | if isinstance(o, str): 1142 | # TODO: some problem with 'string_escape': it escape \n and mess up formating 1143 | # uo = unicode(o.encode('string_escape')) 1144 | # uo = o.decode('latin-1') 1145 | uo = o 1146 | else: 1147 | uo = o 1148 | if isinstance(e, str): 1149 | # TODO: some problem with 'string_escape': it escape \n and mess up formating 1150 | # ue = unicode(e.encode('string_escape')) 1151 | # ue = e.decode('latin-1') 1152 | ue = e 1153 | else: 1154 | ue = e 1155 | 1156 | script = self.REPORT_TEST_OUTPUT_TMPL % dict( 1157 | id=tid, 1158 | output=saxutils.escape(uo + ue), 1159 | ) 1160 | 1161 | # 截图名字通过抛出异常存放在u,通过截取字段获得截图名字 -- Gelomen 1162 | u = uo + ue 1163 | # 先判断是否需要截图 1164 | self.need_screenshot = u.find("errorImg[") 1165 | 1166 | if self.need_screenshot == -1: 1167 | tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL_0 or self.REPORT_TEST_NO_OUTPUT_TMPL 1168 | 1169 | row = tmpl % dict( 1170 | tid=tid, 1171 | Class=(n == 0 and 'hiddenRow' or 'none'), 1172 | style=n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'), 1173 | name=name, 1174 | doc=doc, 1175 | script=script, 1176 | status=self.STATUS[n], 1177 | ) 1178 | else: 1179 | tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL_1 or self.REPORT_TEST_NO_OUTPUT_TMPL 1180 | 1181 | screenshot_list = re.findall(r'errorImg\[(.*?)\]errorImg', u) 1182 | screenshot = "" 1183 | for i in screenshot_list: 1184 | screenshot += "
    img_" + i + "" 1185 | 1186 | # screenshot = u[u.find('errorImg[') + 9:u.find(']errorImg')] 1187 | browser = u[u.find('browser[') + 8:u.find(']browser')] 1188 | 1189 | row = tmpl % dict( 1190 | tid=tid, 1191 | Class=(n == 0 and 'hiddenRow' or 'none'), 1192 | style=n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'), 1193 | name=name, 1194 | doc=doc, 1195 | script=script, 1196 | status=self.STATUS[n], 1197 | # 添加截图字段 1198 | screenshot=screenshot, 1199 | # 添加浏览器版本字段 1200 | browser=browser 1201 | ) 1202 | rows.append(row) 1203 | 1204 | if not has_output: 1205 | return 1206 | 1207 | def _generate_ending(self): 1208 | return self.ENDING_TMPL 1209 | 1210 | 1211 | # 创建文件夹 -- Gelomen 1212 | def create_dir(path="./report", title="Report Title"): 1213 | i = 1.0 1214 | 1215 | dir_path = path + "/" + title + " v" + str(round(i, 1)) 1216 | # 判断文件夹是否存在,不存在则创建 1217 | while True: 1218 | is_dir = os.path.isdir(dir_path) 1219 | if is_dir: 1220 | i += 0.1 1221 | dir_path = path + "/" + title + " v" + str(round(i, 1)) 1222 | else: 1223 | break 1224 | 1225 | os.makedirs(dir_path) 1226 | 1227 | # 测试报告路径 1228 | report_path = dir_path + "/" + title + " v" + str(round(i, 1)) + ".html" 1229 | 1230 | # 将新建的 文件夹路径 和 报告路径 存入全局变量 1231 | set_dir_path(dir_path) 1232 | set_report_path(report_path) 1233 | 1234 | 1235 | # 获取截图并保存 -- Gelomen 1236 | def get_screenshot(browser): 1237 | i = 1 1238 | 1239 | # 通过全局变量获取文件夹路径 1240 | new_dir = get_dir_path() 1241 | if new_dir is not None: 1242 | img_dir = new_dir + "/" + "image" 1243 | # 判断文件夹是否存在,不存在则创建 1244 | is_dir = os.path.isdir(img_dir) 1245 | if not is_dir: 1246 | os.makedirs(img_dir) 1247 | 1248 | img_name = str(i) + ".png" 1249 | img_path = img_dir + "/" + img_name 1250 | 1251 | # 有可能同个测试步骤出错,截图名字一样导致覆盖文件,所以名字存在则增加id 1252 | while True: 1253 | is_file = os.path.isfile(img_path) 1254 | if is_file: 1255 | i += 1 1256 | img_name = str(i) + ".png" 1257 | img_path = img_dir + "/" + img_name 1258 | else: 1259 | break 1260 | 1261 | browser.get_screenshot_as_file(img_path) 1262 | 1263 | browser_type = browser.capabilities["browserName"] 1264 | browser_version = browser.capabilities["browserVersion"] 1265 | browser_msg = browser_type + "(" + browser_version + ")" 1266 | 1267 | print("errorImg[" + img_name + "]errorImg, browser[" + browser_msg + "]browser") 1268 | 1269 | 1270 | # 初始化报告目录 -- Gelomen 1271 | def init(report_dir="./report", title="Test report"): 1272 | def decorator(func): 1273 | def wrapper(*args, **kwargs): 1274 | create_dir(path=report_dir, title=title) 1275 | result = func(*args, **kwargs) 1276 | return result 1277 | 1278 | return wrapper 1279 | 1280 | return decorator 1281 | 1282 | 1283 | # 截图修饰器 -- Gelomen 1284 | def screenshot(func): 1285 | def wrapper(test_object): 1286 | try: 1287 | result = func(test_object) 1288 | return result 1289 | except Exception: 1290 | get_screenshot(test_object.browser) 1291 | raise 1292 | 1293 | return wrapper 1294 | 1295 | 1296 | ############################################################################## 1297 | # Facilities for running tests from the command line 1298 | ############################################################################## 1299 | 1300 | # Note: Reuse unittest.TestProgram to launch test. In the future we may 1301 | # build our own launcher to support more specific command line 1302 | # parameters like test title, CSS, etc. 1303 | class TestProgram(unittest.TestProgram): 1304 | """ 1305 | A variation of the unittest.TestProgram. Please refer to the base 1306 | class for command line parameters. 1307 | """ 1308 | 1309 | def runTests(self): 1310 | # Pick HTMLTestRunner as the default test runner. 1311 | # base class's testRunner parameter is not useful because it means 1312 | # we have to instantiate HTMLTestRunner before we know self.verbosity. 1313 | if self.testRunner is None: 1314 | self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 1315 | unittest.TestProgram.runTests(self) 1316 | 1317 | 1318 | main = TestProgram 1319 | 1320 | ############################################################################## 1321 | # Executing this module from the command line 1322 | ############################################################################## 1323 | 1324 | if __name__ == "__main__": 1325 | main(module=None) 1326 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gelomen/HTMLTestReportCN-ScreenShot/4a6f5b314c528082b03162d685d0bf3dbfc9aed2/lib/__init__.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gelomen/HTMLTestReportCN-ScreenShot/4a6f5b314c528082b03162d685d0bf3dbfc9aed2/requirements.txt -------------------------------------------------------------------------------- /samples/RunAllTests.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """"" 运行 `./testcases` 目录下的所有测试用例,并生成HTML测试报告 """"" 4 | 5 | import unittest 6 | from lib import HTMLTestReportCN 7 | 8 | report_dir = "./report" 9 | title = "自动化测试报告" 10 | description = "测试报告" 11 | test_case_path = "./testcases" 12 | 13 | 14 | @HTMLTestReportCN.init(report_dir=report_dir, title=title) 15 | def run(): 16 | test_suite = unittest.TestLoader().discover(test_case_path) 17 | 18 | fp = open(HTMLTestReportCN.get_report_path(), "wb") 19 | runner = HTMLTestReportCN.HTMLTestRunner( 20 | stream=fp, title=title, description=description, tester=input("请输入你的名字: ") 21 | ) 22 | runner.run(test_suite) 23 | fp.close() 24 | 25 | 26 | if __name__ == "__main__": 27 | run() 28 | -------------------------------------------------------------------------------- /samples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gelomen/HTMLTestReportCN-ScreenShot/4a6f5b314c528082b03162d685d0bf3dbfc9aed2/samples/__init__.py -------------------------------------------------------------------------------- /samples/testcases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gelomen/HTMLTestReportCN-ScreenShot/4a6f5b314c528082b03162d685d0bf3dbfc9aed2/samples/testcases/__init__.py -------------------------------------------------------------------------------- /samples/testcases/test1.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """"" 百度首页测试用例 """"" 4 | 5 | import unittest 6 | from selenium import webdriver 7 | from selenium.webdriver.common.by import By 8 | from lib import HTMLTestReportCN 9 | 10 | 11 | class TestClass(unittest.TestCase): 12 | """ UI自动化测试 """ 13 | 14 | def setUp(self): 15 | self.browser = webdriver.Edge() 16 | self.browser.get("https://www.baidu.com") 17 | 18 | def tearDown(self): 19 | self.browser.quit() 20 | 21 | @HTMLTestReportCN.screenshot 22 | def test1_find_input(self): 23 | """ UI自动化测试1 """ 24 | try: 25 | # 正确值为 "kw" 26 | self.browser.find_element(by=By.ID, value="#kw") 27 | except Exception: 28 | raise 29 | 30 | def test2_assert_equal(self): 31 | """ UI自动化测试2 """ 32 | a = 1 33 | b = 2 34 | try: 35 | self.assertEqual(a, b, "a ≠ b!") 36 | except AssertionError: 37 | raise 38 | 39 | @HTMLTestReportCN.screenshot 40 | def test3_title(self): 41 | """ UI自动化测试3 """ 42 | title = self.browser.title 43 | try: 44 | # 加了个感叹号 ! 45 | self.assertEqual(title, "百度一下,你就知道!", "Title不一致!") 46 | except AssertionError: 47 | raise 48 | 49 | 50 | if __name__ == "__main__": 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /samples/testcases/test2.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """"" 数据验证测试用例 """"" 4 | 5 | import unittest 6 | 7 | 8 | class TestClass(unittest.TestCase): 9 | """ 数据自动化测试 """ 10 | 11 | def setUp(self): 12 | self.a = 1 13 | self.b = 2 14 | 15 | def tearDown(self): 16 | pass 17 | 18 | def test1(self): 19 | """ 数据验证1 """ 20 | try: 21 | self.assertNotEqual(self.a, self.b, "a == b!") 22 | except AssertionError: 23 | raise 24 | 25 | def test2(self): 26 | """ 数据验证2 """ 27 | try: 28 | self.assertEqual(self.a, self.b, "a ≠ b!") 29 | except AssertionError: 30 | raise 31 | 32 | 33 | if __name__ == "__main__": 34 | unittest.main() 35 | --------------------------------------------------------------------------------