├── .gitattributes ├── .gitignore ├── HTMLTestRunner_cn.py ├── README.md ├── changelog.md ├── img ├── 1.png ├── 2.png ├── 3.png ├── 4.gif ├── 5.gif ├── 5.png ├── 6.png ├── 7.png └── 8.png ├── sample_test_report.html ├── sample_test_report_appium.html ├── test_screenshot_appium.py └── test_screenshot_selenium.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py linguist-language=Python 2 | *.html linguist-language=Python -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | 126 | .idea/* 127 | /.idea/* 128 | Untitled.ipynb 129 | /.ipynb_checkpoints/ 130 | -------------------------------------------------------------------------------- /HTMLTestRunner_cn.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 | 68 | __author__ = "Wai Yip Tung" 69 | __version__ = "0.8.3" 70 | 71 | 72 | """ 73 | Change History 74 | Version 0.8.4 by GoverSky 75 | * Add sopport for 3.x 76 | * Add piechart for resultpiechart 77 | * Add Screenshot for selenium_case test 78 | * Add Retry on failed 79 | 80 | Version 0.8.3 81 | * Prevent crash on class or module-level exceptions (Darren Wurf). 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 419 | %(heading)s 420 |
421 |
422 | 423 |
424 |
425 | %(report)s 426 | %(ending)s 427 | 428 | 429 | 430 | """ 431 | # variables: (title, generator, stylesheet, heading, report, ending) 432 | 433 | 434 | # ------------------------------------------------------------------------ 435 | # Stylesheet 436 | # 437 | # alternatively use a for external style sheet, e.g. 438 | # 439 | 440 | STYLESHEET_TMPL = """ 441 | 644 | """ 645 | 646 | # ------------------------------------------------------------------------ 647 | # Heading 648 | # 649 | 650 | HEADING_TMPL = """
651 |

%(title)s

652 | %(parameters)s 653 |

%(description)s

654 |
655 | 656 | """ # variables: (title, parameters, description) 657 | 658 | HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

659 | """ # variables: (name, value) 660 | 661 | # ------------------------------------------------------------------------ 662 | # Report 663 | # 664 | 665 | REPORT_TMPL = """ 666 |
667 | 概要[%(Pass_p).2f%%] 668 | 通过[%(Pass)s] 669 | 失败[%(fail)s] 670 | 错误[%(error)s] 671 | 跳过[%(skip)s] 672 | 所有[%(total)s] 673 |
674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | %(test_list)s 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 |
测试组/测试用例总数通过失败错误视图错误截图
统计%(count)s%(Pass)s%(fail)s%(error)s  
705 | 709 | """ 710 | # variables: (test_list, count, Pass, fail, error) 711 | 712 | REPORT_CLASS_TMPL = r""" 713 | 714 | %(desc)s 715 | %(count)s 716 | %(Pass)s 717 | %(fail)s 718 | %(error)s 719 | 详情 720 |   721 | 722 | """ # variables: (style, desc, count, Pass, fail, error, cid) 723 | 724 | REPORT_TEST_WITH_OUTPUT_TMPL = r""" 725 | 726 |
%(desc)s
727 | 728 | 729 | 730 | 731 | 732 | %(status)s 733 | 734 | 743 | 744 | 745 | 746 | %(img)s 747 | 748 | """ # variables: (tid, Class, style, desc, status,img) 749 | 750 | REPORT_TEST_NO_OUTPUT_TMPL = r""" 751 | 752 |
%(desc)s
753 | %(status)s 754 | %(img)s 755 | 756 | """ # variables: (tid, Class, style, desc, status,img) 757 | 758 | REPORT_TEST_OUTPUT_TMPL = r""" 759 | %(id)s: %(output)s 760 | """ # variables: (id, output) 761 | 762 | 763 | IMG_TMPL = r""" 764 | 显示截图 765 | 770 | """ 771 | # ------------------------------------------------------------------------ 772 | # ENDING 773 | # 774 | 775 | ENDING_TMPL = """
 
""" 776 | 777 | # -------------------- The end of the Template class ------------------- 778 | 779 | def __getattribute__(self, item): 780 | value = object.__getattribute__(self, item) 781 | if PY3K: 782 | return value 783 | else: 784 | if isinstance(value, str): 785 | return value.decode("utf-8") 786 | else: 787 | return value 788 | 789 | 790 | TestResult = unittest.TestResult 791 | 792 | 793 | class _TestResult(TestResult): 794 | # note: _TestResult is a pure representation of results. 795 | # It lacks the output and reporting ability compares to unittest._TextTestResult. 796 | shouldStop=False 797 | def __init__(self, verbosity=1, retry=0,save_last_try=False): 798 | TestResult.__init__(self) 799 | 800 | self.stdout0 = None 801 | self.stderr0 = None 802 | self.success_count = 0 803 | self.failure_count = 0 804 | self.error_count = 0 805 | self.skip_count = 0 806 | self.verbosity = verbosity 807 | 808 | # result is a list of result in 4 tuple 809 | # ( 810 | # result code (0: success; 1: fail; 2: error;3:skip), 811 | # TestCase object, 812 | # Test output (byte string), 813 | # stack trace, 814 | # ) 815 | self.result = [] 816 | self.retry = retry 817 | self.trys = 0 818 | self.status = 0 819 | 820 | self.save_last_try = save_last_try 821 | self.outputBuffer = StringIO.StringIO() 822 | 823 | def startTest(self, test): 824 | # test.imgs = [] 825 | test.imgs = getattr(test, "imgs", []) 826 | # TestResult.startTest(self, test) 827 | self.outputBuffer.seek(0) 828 | self.outputBuffer.truncate() 829 | stdout_redirector.fp = self.outputBuffer 830 | stderr_redirector.fp = self.outputBuffer 831 | self.stdout0 = sys.stdout 832 | self.stderr0 = sys.stderr 833 | sys.stdout = stdout_redirector 834 | sys.stderr = stderr_redirector 835 | 836 | def complete_output(self): 837 | """ 838 | Disconnect output redirection and return buffer. 839 | Safe to call multiple times. 840 | """ 841 | if self.stdout0: 842 | sys.stdout = self.stdout0 843 | sys.stderr = self.stderr0 844 | self.stdout0 = None 845 | self.stderr0 = None 846 | return self.outputBuffer.getvalue() 847 | 848 | def stopTest(self, test): 849 | # Usually one of addSuccess, addError or addFailure would have been called. 850 | # But there are some path in unittest that would bypass this. 851 | # We must disconnect stdout in stopTest(), which is guaranteed to be called. 852 | if self.retry and self.retry>=1: 853 | if self.status == 1: 854 | self.trys += 1 855 | if self.trys <= self.retry: 856 | if self.save_last_try: 857 | t = self.result.pop(-1) 858 | if t[0]==1: 859 | self.failure_count -=1 860 | else: 861 | self.error_count -= 1 862 | test=copy.copy(test) 863 | sys.stderr.write("Retesting... ") 864 | sys.stderr.write(str(test)) 865 | sys.stderr.write('..%d \n' % self.trys) 866 | doc = getattr(test,'_testMethodDoc',u"") or u'' 867 | if doc.find('_retry')!=-1: 868 | doc = doc[:doc.find('_retry')] 869 | desc ="%s_retry:%d" %(doc, self.trys) 870 | if not PY3K: 871 | if isinstance(desc, str): 872 | desc = desc.decode("utf-8") 873 | test._testMethodDoc = desc 874 | test(self) 875 | else: 876 | self.status = 0 877 | self.trys = 0 878 | self.complete_output() 879 | 880 | def addSuccess(self, test): 881 | self.success_count += 1 882 | self.status = 0 883 | TestResult.addSuccess(self, test) 884 | output = self.complete_output() 885 | self.result.append((0, test, output, '')) 886 | if self.verbosity > 1: 887 | sys.stderr.write('P ') 888 | sys.stderr.write(str(test)) 889 | sys.stderr.write('\n') 890 | else: 891 | sys.stderr.write('P') 892 | 893 | def addFailure(self, test, err): 894 | self.failure_count += 1 895 | self.status = 1 896 | TestResult.addFailure(self, test, err) 897 | _, _exc_str = self.failures[-1] 898 | output = self.complete_output() 899 | self.result.append((1, test, output, _exc_str)) 900 | if not getattr(test, "driver",""): 901 | pass 902 | else: 903 | try: 904 | driver = getattr(test, "driver") 905 | test.imgs.append(driver.get_screenshot_as_base64()) 906 | except Exception as e: 907 | pass 908 | if self.verbosity > 1: 909 | sys.stderr.write('F ') 910 | sys.stderr.write(str(test)) 911 | sys.stderr.write('\n') 912 | else: 913 | sys.stderr.write('F') 914 | 915 | def addError(self, test, err): 916 | self.error_count += 1 917 | self.status = 1 918 | TestResult.addError(self, test, err) 919 | _, _exc_str = self.errors[-1] 920 | output = self.complete_output() 921 | self.result.append((2, test, output, _exc_str)) 922 | if not getattr(test, "driver",""): 923 | pass 924 | else: 925 | try: 926 | driver = getattr(test, "driver") 927 | test.imgs.append(driver.get_screenshot_as_base64()) 928 | except Exception: 929 | pass 930 | if self.verbosity > 1: 931 | sys.stderr.write('E ') 932 | sys.stderr.write(str(test)) 933 | sys.stderr.write('\n') 934 | else: 935 | sys.stderr.write('E') 936 | 937 | def addSkip(self, test, reason): 938 | self.skip_count += 1 939 | self.status = 0 940 | TestResult.addSkip(self, test,reason) 941 | output = self.complete_output() 942 | self.result.append((3, test, output, reason)) 943 | if self.verbosity > 1: 944 | sys.stderr.write('K') 945 | sys.stderr.write(str(test)) 946 | sys.stderr.write('\n') 947 | else: 948 | sys.stderr.write('K') 949 | 950 | class HTMLTestRunner(Template_mixin): 951 | def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None,is_thread=False, retry=0,save_last_try=True): 952 | self.stream = stream 953 | self.retry = retry 954 | self.is_thread=is_thread 955 | self.threads= 5 956 | self.save_last_try=save_last_try 957 | self.verbosity = verbosity 958 | self.run_times=0 959 | if title is None: 960 | self.title = self.DEFAULT_TITLE 961 | else: 962 | self.title = title 963 | if description is None: 964 | self.description = self.DEFAULT_DESCRIPTION 965 | else: 966 | self.description = description 967 | 968 | def run(self, test): 969 | "Run the given test case or test suite." 970 | self.startTime = datetime.datetime.now() 971 | result = _TestResult(self.verbosity, self.retry, self.save_last_try) 972 | test(result) 973 | self.stopTime = datetime.datetime.now() 974 | self.run_times+=1 975 | self.generateReport(result) 976 | if PY3K: 977 | # for python3 978 | # print('\nTime Elapsed: %s' % (self.stopTime - self.startTime),file=sys.stderr) 979 | output = '\nTime Elapsed: %s' % (self.stopTime - self.startTime) 980 | sys.stderr.write(output) 981 | else: 982 | # for python2 983 | print >> sys.stderr, '\nTime Elapsed: %s' % (self.stopTime - self.startTime) 984 | return result 985 | 986 | def sortResult(self, result_list): 987 | # unittest does not seems to run in any particular order. 988 | # Here at least we want to group them together by class. 989 | rmap = {} 990 | classes = [] 991 | for n, t, o, e in result_list: 992 | cls = t.__class__ 993 | if not cls in rmap: 994 | rmap[cls] = [] 995 | classes.append(cls) 996 | rmap[cls].append((n, t, o, e)) 997 | for cls in classes: 998 | rmap[cls].sort(key=cmp_to_key(lambda a,b:1 if a[1].id()>b[1].id() else ( 1 if a[1].id()==b[1].id() else -1))) 999 | r = [(cls, rmap[cls]) for cls in classes] 1000 | # name = t.id().split('.')[-1] 1001 | r.sort(key=cmp_to_key(lambda a, b: 1 if a[0].__name__ > b[0].__name__ else -1)) 1002 | return r 1003 | 1004 | def getReportAttributes(self, result): 1005 | """ 1006 | Return report attributes as a list of (name, value). 1007 | Override this to add custom attributes. 1008 | """ 1009 | startTime = str(self.startTime)[:19] 1010 | duration = str(self.stopTime - self.startTime) 1011 | status = [] 1012 | if result.success_count: 1013 | status.append(u'Pass:%s' % result.success_count) 1014 | if result.failure_count: 1015 | status.append(u'Failure:%s' % result.failure_count) 1016 | if result.error_count: 1017 | status.append(u'Error:%s' % result.error_count) 1018 | if result.skip_count: 1019 | status.append(u'Skip:%s' % result.skip_count) 1020 | total = result.success_count+result.failure_count+result.error_count + result.skip_count 1021 | if total>0: 1022 | passed = result.success_count*1.000/total*100 1023 | else: 1024 | passed =0.0 1025 | status.append(u'通过率:%.1f%%' % passed) 1026 | if status: 1027 | status = u' '.join(status) 1028 | else: 1029 | status = 'none' 1030 | return [ 1031 | (u'开始时间', startTime), 1032 | (u'耗时', duration), 1033 | (u'状态', status), 1034 | ] 1035 | 1036 | def generateReport(self, result): 1037 | report_attrs = self.getReportAttributes(result) 1038 | generator = 'HTMLTestRunner %s' % __version__ 1039 | stylesheet = self._generate_stylesheet() 1040 | heading = self._generate_heading(report_attrs) 1041 | report = self._generate_report(result) 1042 | ending = self._generate_ending() 1043 | output = self.HTML_TMPL % dict( 1044 | title=saxutils.escape(self.title), 1045 | generator=generator, 1046 | stylesheet=stylesheet, 1047 | heading=heading, 1048 | report=report, 1049 | ending=ending, 1050 | channel=self.run_times, 1051 | ) 1052 | if PY3K: 1053 | self.stream.write(output.encode()) 1054 | else: 1055 | self.stream.write(output.encode('utf8')) 1056 | 1057 | def _generate_stylesheet(self): 1058 | return self.STYLESHEET_TMPL 1059 | 1060 | def _generate_heading(self, report_attrs): 1061 | a_lines = [] 1062 | for name, value in report_attrs: 1063 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1064 | name=name, 1065 | value=value, 1066 | ) 1067 | a_lines.append(line) 1068 | heading = self.HEADING_TMPL % dict( 1069 | title=saxutils.escape(self.title), 1070 | parameters=''.join(a_lines), 1071 | description=saxutils.escape(self.description), 1072 | ) 1073 | return heading 1074 | 1075 | def _generate_report(self, result): 1076 | rows = [] 1077 | sortedResult = self.sortResult(result.result) 1078 | for cid, (cls, cls_results) in enumerate(sortedResult): 1079 | # subtotal for a class 1080 | np = nf = ne = ns = 0 1081 | for n, t, o, e in cls_results: 1082 | if n == 0: 1083 | np += 1 1084 | elif n == 1: 1085 | nf += 1 1086 | elif n==2: 1087 | ne += 1 1088 | else: 1089 | ns +=1 1090 | 1091 | # format class description 1092 | if cls.__module__ == "__main__": 1093 | name = cls.__name__ 1094 | else: 1095 | name = "%s.%s" % (cls.__module__, cls.__name__) 1096 | doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 1097 | desc = doc and '%s: %s' % (name, doc) or name 1098 | if not PY3K: 1099 | if isinstance(desc, str): 1100 | desc = desc.decode("utf-8") 1101 | 1102 | row = self.REPORT_CLASS_TMPL % dict( 1103 | style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 1104 | desc=desc, 1105 | count=np + nf + ne, 1106 | Pass=np, 1107 | fail=nf, 1108 | error=ne, 1109 | cid='c%s.%s' % (self.run_times,cid + 1), 1110 | ) 1111 | rows.append(row) 1112 | 1113 | for tid, (n, t, o, e) in enumerate(cls_results): 1114 | self._generate_report_test(rows, cid, tid, n, t, o, e) 1115 | total = result.success_count + result.failure_count + result.error_count +result.skip_count 1116 | report = self.REPORT_TMPL % dict( 1117 | test_list=u''.join(rows), 1118 | count=str(total), 1119 | Pass=str(result.success_count), 1120 | Pass_p=result.success_count*1.00/total*100 if total else 0.0, 1121 | fail=str(result.failure_count), 1122 | error=str(result.error_count), 1123 | skip=str(result.skip_count), 1124 | total=str(total), 1125 | channel=str(self.run_times), 1126 | ) 1127 | return report 1128 | 1129 | def _generate_report_test(self, rows, cid, tid, n, t, o, e): 1130 | # e.g. 'pt1.1', 'ft1.1', etc 1131 | has_output = bool(o or e) 1132 | if n==0: 1133 | tmp="p" 1134 | elif n==1: 1135 | tmp="f" 1136 | elif n==2: 1137 | tmp = "e" 1138 | else: 1139 | tmp = "s" 1140 | tid = tmp + 't%d.%d.%d' % (self.run_times,cid + 1, tid + 1) 1141 | name = t.id().split('.')[-1] 1142 | if self.verbosity > 1: 1143 | doc = getattr(t,'_testMethodDoc',"") or '' 1144 | else: 1145 | doc = "" 1146 | 1147 | desc = doc and ('%s: %s' % (name, doc)) or name 1148 | if not PY3K: 1149 | if isinstance(desc, str): 1150 | desc = desc.decode("utf-8") 1151 | tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 1152 | 1153 | # o and e should be byte string because they are collected from stdout and stderr? 1154 | if isinstance(o, str): 1155 | # uo = unicode(o.encode('string_escape')) 1156 | if PY3K: 1157 | uo = o 1158 | else: 1159 | uo = o.decode('utf-8', 'ignore') 1160 | else: 1161 | uo = o 1162 | if isinstance(e, str): 1163 | # ue = unicode(e.encode('string_escape')) 1164 | if PY3K: 1165 | ue = e 1166 | elif e.find("Error") != -1 or e.find("Exception") != -1: 1167 | es = e.decode('utf-8', 'ignore').split('\n') 1168 | try: 1169 | if es[-2].find("\\u") != -1 or es[-2].find('"\\u') != -1: 1170 | es[-2] = es[-2].decode('unicode_escape') 1171 | except Exception: 1172 | pass 1173 | ue = u"\n".join(es) 1174 | else: 1175 | ue = e.decode('utf-8', 'ignore') 1176 | else: 1177 | ue = e 1178 | 1179 | script = self.REPORT_TEST_OUTPUT_TMPL % dict( 1180 | id=tid, 1181 | output=saxutils.escape(uo + ue), 1182 | ) 1183 | if getattr(t,'imgs',[]): 1184 | # 判断截图列表,如果有则追加 1185 | tmp = u"" 1186 | for i, img in enumerate(t.imgs): 1187 | if i==0: 1188 | tmp+=""" \n""" % img 1189 | else: 1190 | tmp+=""" \n""" % img 1191 | imgs = self.IMG_TMPL % dict(imgs=tmp) 1192 | else: 1193 | imgs = u"""无截图""" 1194 | 1195 | row = tmpl % dict( 1196 | tid=tid, 1197 | Class=(n == 0 and 'hiddenRow' or 'none'), 1198 | style=n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'), 1199 | desc=desc, 1200 | script=script, 1201 | status=self.STATUS[n], 1202 | img=imgs, 1203 | ) 1204 | rows.append(row) 1205 | if not has_output: 1206 | return 1207 | 1208 | def _generate_ending(self): 1209 | return self.ENDING_TMPL 1210 | 1211 | 1212 | ############################################################################## 1213 | # Facilities for running tests from the command line 1214 | ############################################################################## 1215 | 1216 | # Note: Reuse unittest.TestProgram to launch test. In the future we may 1217 | # build our own launcher to support more specific command line 1218 | # parameters like test title, CSS, etc. 1219 | class TestProgram(unittest.TestProgram): 1220 | """ 1221 | A variation of the unittest.TestProgram. Please refer to the base 1222 | class for command line parameters. 1223 | """ 1224 | 1225 | def runTests(self): 1226 | # Pick HTMLTestRunner as the default test runner. 1227 | # base class's testRunner parameter is not useful because it means 1228 | # we have to instantiate HTMLTestRunner before we know self.verbosity. 1229 | if self.testRunner is None: 1230 | self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 1231 | unittest.TestProgram.runTests(self) 1232 | 1233 | 1234 | main = TestProgram 1235 | 1236 | ############################################################################## 1237 | # Executing this module from the command line 1238 | ############################################################################## 1239 | 1240 | if __name__ == "__main__": 1241 | main(module=None) 1242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTMLTestRunner 汉化版 2 | 在原版的基础上进行扩展和改造 3 | 4 | # 当年改造初衷 5 | + 方便自己做汉化报告生成 6 | + 对自己积累知识的检验 7 | + 挑战下单文件报告都能做出什么花样 8 | 近两年不怎么搞UI自动化了,项目就一直没怎么更新(pytest香啊😅) 9 | 10 | # todo 11 | + 多线程/多进程执行用例(数据统计逻辑要重新设计,还有兼容性问题😑) 12 | + UI 美化 (通过CDN集成一些成熟的js库~然后加5毛钱特效😜) 13 | + 与ddt的集成(目测基本就把源码收进来😏) 14 | 15 | 16 | 17 | # 报告汉化,错误日志 18 | ![](./img/1.png) 19 | # selenium/appium 截图 20 | 截图功能根据测试结果,当结果为fail或error时自动截图
21 | 截图方法在_TestResult 的测试结果收集中,报告使用的截图全部保存为base64编码,避免了报告图片附件的问题,可以根据自己使用的框架不同自行调整,selenium 使用的是get_screenshot_as_base64 方法获取页面截图的base64编码
22 | ![](./img/2.png) 23 | 因为要提取用例中的driver变量获取webdriver对象,所以要实现截图功能必须定义在用例中定义webdriver 为driver 24 | ```python 25 | def setUp(self): 26 | self.imgs=[] # (可选)初始化截图列表 27 | self.driver = webdriver.Chrome() 28 | ``` 29 | 或者 30 | ```python 31 | @classmethod 32 | def setUpClass(cls): 33 | cls.driver = webdriver.Chrome() 34 | ``` 35 | 也可以在测试过程中某一步骤自定义添加截图,比如
36 | ![](./img/3.png)
37 | 生成报告后会统一进行展示
38 | **Selenium截图轮播效果**
39 | ![](./img/4.gif)
40 | **Appium效果轮播截图**
41 | ![](./img/5.gif) 42 | # 用例失败重试 43 | 根据unittest的运行机制,在stopTest 中判断测试结果,如果失败或出错status为1,判断是否需要重试;
44 | ![](./img/5.png) 45 | 46 | 在实例化HTMLTestRunner 对象时追加参数,retry,指定重试次数,如果save_last_try 为True ,一个用例仅显示最后一次测试的结果。 47 | ```python 48 | HTMLTestRunner(title="带截图的测试报告", description="小试牛刀", stream=open("sample_test_report.html", "wb"), verbosity=2, retry=2, save_last_try=True) 49 | ``` 50 | 51 | ![](./img/6.png) 52 | 如果save_last_try 为False,则显示所有重试的结果。 53 | ```python 54 | HTMLTestRunner(title="带截图的测试报告", description="小试牛刀", stream=open("sample_test_report.html", "wb"), verbosity=2, retry=2, save_last_try=False) 55 | ``` 56 | 57 | ![](./img/7.png) 58 | 运行中输出效果如下:
59 | ![](./img/8.png) 60 | 61 | `注意:在python3 中因为unittest运行机制变动,在使用setUp/tearDown中初始化/退出driver时,会出现用例执行失败没有截图的问题,所以推荐使用样例中setUpClass/tearDownClass的用法` 62 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # changelog 2 | 3 | + 20170925 4 | - 测试报告完全汉化,包括错误日志的中文处理 5 | - 针对selenium UI测试增加失败自动截图功能,截图自动轮播 6 | - 增加失败自动重试功能 7 | - 增加饼图统计 8 | - 同时兼容python2.x 和3.x 9 | + 20180402 10 | - 表格样式优化 11 | - 修复部分bug 12 | - 增加截图组,可展示多张截图,首次打开自动播放 13 | - 增加仅展示最后一次运行结果,多次重试时,每个测试用例仅展示一次 14 | + 20181213 15 | - 增加分类标签、通过率等,优化样式 16 | - 修复部分框架在SetUP中失败导致测试中断的问题导致 ErrorHandle的问题 17 | - 修复部分编码Bug 18 | - 优化运行逻辑 19 | - 对js代码优化,修复部分多次运行run导致结果异常的bug 20 | 21 | + 20200427 22 | - 修复页面小错误 (fzk27) 23 | 24 | + 20200508 25 | - 开放跳过测试的统计,完善饼图统计 -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoverSky/HTMLTestRunner_cn/1792ed420e2aa4177166d33405be94019d6587f3/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoverSky/HTMLTestRunner_cn/1792ed420e2aa4177166d33405be94019d6587f3/img/2.png -------------------------------------------------------------------------------- /img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoverSky/HTMLTestRunner_cn/1792ed420e2aa4177166d33405be94019d6587f3/img/3.png -------------------------------------------------------------------------------- /img/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoverSky/HTMLTestRunner_cn/1792ed420e2aa4177166d33405be94019d6587f3/img/4.gif -------------------------------------------------------------------------------- /img/5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoverSky/HTMLTestRunner_cn/1792ed420e2aa4177166d33405be94019d6587f3/img/5.gif -------------------------------------------------------------------------------- /img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoverSky/HTMLTestRunner_cn/1792ed420e2aa4177166d33405be94019d6587f3/img/5.png -------------------------------------------------------------------------------- /img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoverSky/HTMLTestRunner_cn/1792ed420e2aa4177166d33405be94019d6587f3/img/6.png -------------------------------------------------------------------------------- /img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoverSky/HTMLTestRunner_cn/1792ed420e2aa4177166d33405be94019d6587f3/img/7.png -------------------------------------------------------------------------------- /img/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoverSky/HTMLTestRunner_cn/1792ed420e2aa4177166d33405be94019d6587f3/img/8.png -------------------------------------------------------------------------------- /test_screenshot_appium.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2017/9/6 11:26 3 | # @File : aaa.py 4 | # @Author : 守望@天空~ 5 | """HTMLTestRunner 截图版示例 appium版""" 6 | from appium import webdriver 7 | import unittest 8 | from HTMLTestRunner_cn import HTMLTestRunner 9 | 10 | 11 | class case_01(unittest.TestCase): 12 | 13 | @classmethod 14 | def setUpClass(cls): 15 | desired_caps = {} 16 | desired_caps['platformName'] = 'Android' 17 | desired_caps['platformVersion'] = '4.4.2' 18 | desired_caps['deviceName'] = 'Android Emulator' 19 | desired_caps['app'] = 'com.tencent.mobileqq' 20 | desired_caps['appActivity'] = 'com.tencent.mobileqq.activity.SplashActivity' 21 | cls.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) 22 | 23 | @classmethod 24 | def tearDownClass(cls): 25 | cls.driver.quit() 26 | 27 | 28 | def add_img(self): 29 | # 在是python3.x 中,如果在这里初始化driver ,因为3.x版本 unittest 运行机制不同,会导致用力失败时截图失败 30 | self.imgs.append(self.driver.get_screenshot_as_base64()) 31 | return True 32 | 33 | def setUp(self): 34 | self.imgs = [] 35 | self.addCleanup(self.cleanup) 36 | 37 | def cleanup(self): 38 | pass 39 | 40 | 41 | def test_case1(self): 42 | """ 手机QQ截图""" 43 | self.add_img() 44 | self.add_img() 45 | self.add_img() 46 | self.add_img() 47 | self.add_img() 48 | 49 | 50 | 51 | if __name__ == "__main__": 52 | suite = unittest.TestLoader().loadTestsFromTestCase(case_01) 53 | runer = HTMLTestRunner(title="带截图的测试报告", description="小试牛刀", stream=open("sample_test_report_appium.html", "wb"), verbosity=2, retry=1, save_last_try=True) 54 | runer.run(suite) 55 | -------------------------------------------------------------------------------- /test_screenshot_selenium.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2017/9/6 11:26 3 | # @File : aaa.py 4 | # @Author : 守望@天空~ 5 | """HTMLTestRunner 截图版示例 selenium 版""" 6 | from selenium import webdriver 7 | import unittest 8 | import time 9 | from HTMLTestRunner_cn import HTMLTestRunner 10 | import sys 11 | 12 | 13 | class case_01(unittest.TestCase): 14 | """ 15 | def setUp(cls): 16 | cls.driver = webdriver.Chrome() 17 | 18 | def tearDown(cls): 19 | cls.driver.quit() 20 | 21 | """ 22 | @classmethod 23 | def setUpClass(cls): 24 | cls.driver = webdriver.Chrome() 25 | 26 | @classmethod 27 | def tearDownClass(cls): 28 | cls.driver.quit() 29 | 30 | def add_img(self): 31 | # self.imgs.append(self.driver.get_screenshot_as_base64()) 32 | return True 33 | 34 | def setUp(self): 35 | # 在python3.x 中,如果在这里初始化driver ,因为3.x版本 unittest 运行机制不同,会导致用例失败后截图失败 36 | self.imgs = [] 37 | self.addCleanup(self.cleanup) 38 | 39 | def cleanup(self): 40 | pass 41 | 42 | 43 | def test_case1(self): 44 | """ 百度搜索 45 | 呵呵呵呵 46 | """ 47 | print("本次校验没过?") 48 | print ("超级长"*66) 49 | self.driver.get("https://www.baidu.com") 50 | self.add_img() 51 | self.driver.find_element_by_id('kw').send_keys(u'百度一下') 52 | self.add_img() 53 | self.driver.find_element_by_id('su').click() 54 | time.sleep(1) 55 | self.add_img() 56 | 57 | def test_case2(self): 58 | """搜狗首页""" 59 | self.driver.get("http://www.sogou.com") 60 | print("本次校验没过?") 61 | self.assertTrue(False,"这是相当的睿智了") 62 | 63 | def test_case3(self): 64 | """ QQ邮箱""" 65 | self.driver.get("https://mail.qq.com") 66 | # self.imgs.append(self.driver.get_screenshot_as_base64()) 67 | print("没法打印?") 68 | self.assertIn(u"中文", u'中华','小当家?') 69 | 70 | def test_case4(self): 71 | u""" 淘宝""" 72 | self.driver.get("http://www.taobao.com/") 73 | raise Exception 74 | self.add_img() 75 | self.assertTrue(True) 76 | 77 | 78 | class case_02(unittest.TestCase): 79 | """ 80 | def setUp(cls): 81 | cls.driver = webdriver.Chrome() 82 | 83 | def tearDown(cls): 84 | cls.driver.quit() 85 | 86 | """ 87 | @classmethod 88 | def setUpClass(cls): 89 | cls.driver = webdriver.Chrome() 90 | 91 | @classmethod 92 | def tearDownClass(cls): 93 | cls.driver.quit() 94 | 95 | def add_img(self): 96 | # self.imgs.append(self.driver.get_screenshot_as_base64()) 97 | return True 98 | 99 | def setUp(self): 100 | # 在是python3.x 中,如果在这里初始化driver ,因为3.x版本 unittest 运行机制不同,会导致用力失败时截图失败 101 | self.imgs = [] 102 | self.addCleanup(self.cleanup) 103 | 104 | def cleanup(self): 105 | pass 106 | 107 | 108 | def test_case1(self): 109 | """ 百度搜索 110 | 呵呵呵呵 111 | """ 112 | print("校验了一下") 113 | self.driver.get("https://www.baidu.com") 114 | self.add_img() 115 | self.driver.find_element_by_id('kw').send_keys(u'百度一下') 116 | self.add_img() 117 | self.driver.find_element_by_id('su').click() 118 | time.sleep(1) 119 | self.add_img() 120 | 121 | @unittest.skip('跳过') 122 | def test_case2(self): 123 | """搜狗首页""" 124 | self.driver.get("http://www.sogou.com") 125 | print("本次校验没过?") 126 | self.assertTrue(False,"这是相当的睿智了") 127 | 128 | def test_case3(self): 129 | """ QQ邮箱""" 130 | self.driver.get("https://mail.qq.com") 131 | # self.imgs.append(self.driver.get_screenshot_as_base64()) 132 | print("没法打印?") 133 | self.assertIn(u"中文", u'中文') 134 | 135 | def test_case4(self): 136 | u""" 淘宝""" 137 | self.driver.get("http://www.taobao.com/") 138 | self.add_img() 139 | self.assertTrue(True) 140 | 141 | 142 | if __name__ == "__main__": 143 | from unittest import TestResult 144 | suite1 = unittest.TestLoader().loadTestsFromTestCase(case_01) 145 | suite2 = unittest.TestLoader().loadTestsFromTestCase(case_02) 146 | suites = unittest.TestSuite() 147 | suites.addTests([suite1,suite2]) 148 | 149 | runer = HTMLTestRunner(title="带截图的测试报告", description="小试牛刀", stream=open("sample_test_report.html", "wb"), verbosity=2, retry=2, save_last_try=True) 150 | runer.run(suites) 151 | --------------------------------------------------------------------------------