├── .gitattributes ├── .idea └── vcs.xml ├── HTMLTestRunner_Chart.py ├── README.md ├── demo.html ├── demo.json ├── img ├── 收款码1.png ├── 显示截图1.png ├── 走势图1.png ├── 饼图1.png └── 首页1.png ├── license └── test_selenium.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=Python 2 | *.css linguist-language=Python 3 | *.html linguist-language=Python 4 | *.vue linguist-language=Python -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /HTMLTestRunner_Chart.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 | import os 68 | 69 | __author__ = "Wai Yip Tung" 70 | __version__ = "0.9.1" 71 | 72 | """ 73 | Change History 74 | Version 0.9.1 75 | * 用Echarts添加执行情况统计图 (灰蓝) 76 | 77 | Version 0.9.0 78 | * 改成Python 3.x (灰蓝) 79 | 80 | Version 0.8.3 81 | * 使用 Bootstrap稍加美化 (灰蓝) 82 | * 改为中文 (灰蓝) 83 | 84 | Version 0.8.2 85 | * Show output inline instead of popup window (Viorel Lupu). 86 | 87 | Version in 0.8.1 88 | * Validated XHTML (Wolfgang Borgert). 89 | * Added description of test classes and test cases. 90 | 91 | Version in 0.8.0 92 | * Define Template_mixin class for customization. 93 | * Workaround a IE 6 bug that it does not treat 210 | 211 | 212 | 213 | 214 | %(stylesheet)s 215 | 216 | 217 | 218 | 529 | 530 |
531 | %(heading)s 532 | %(report)s 533 | %(ending)s 534 | %(chart_script)s 535 |
536 | 537 | 538 | """ # variables: (title, generator, stylesheet, heading, report, ending, chart_script) 539 | 540 | ECHARTS_SCRIPT = """ 541 | 586 | """ # variables: (Pass, fail, error) 587 | 588 | # ------------------------------------------------------------------------ 589 | # Stylesheet 590 | # 591 | # alternatively use a for external style sheet, e.g. 592 | # 593 | 594 | STYLESHEET_TMPL = """ 595 | 727 | """ 728 | 729 | # ------------------------------------------------------------------------ 730 | # Heading 731 | # 732 | 733 | HEADING_TMPL = """ 734 | 738 | 739 |
740 |
741 | 744 |

*注: 只保留最近十次的测试记录

745 | """ # variables: (title, parameters, description) 746 | 747 | HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

748 | """ # variables: (name, value) 749 | 750 | # ------------------------------------------------------------------------ 751 | # Report 752 | # 753 | 754 | REPORT_TMPL = u""" 755 |
756 |
757 | 概要{ %(passrate)s } 758 | 错误{ %(error)s } 759 | 失败{ %(fail)s } 760 | 通过{ %(Pass)s } 761 | 所有{ %(count)s } 762 |
763 |

764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | %(test_list)s 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 |
测试套件/测试用例总数通过失败错误视图错误截图
总计%(count)s%(Pass)s%(fail)s%(error)s  
794 |
795 | """ # variables: (test_list, count, Pass, fail, error) 796 | 797 | REPORT_CLASS_TMPL = u""" 798 | 799 | %(desc)s 800 | %(count)s 801 | %(Pass)s 802 | %(fail)s 803 | %(error)s 804 | 详情 805 |   806 | 807 | """ # variables: (style, desc, count, Pass, fail, error, cid) 808 | 809 | REPORT_TEST_WITH_OUTPUT_TMPL = r""" 810 | 811 |
%(desc)s
812 | 813 | 814 | 815 | 816 | %(status)s 817 | 818 | 825 | 826 | 827 | 828 | %(img)s 829 | 830 | """ # variables: (tid, Class, style, desc, status) 831 | 832 | REPORT_TEST_NO_OUTPUT_TMPL = r""" 833 | 834 |
%(desc)s
835 | %(status)s 836 | %(img)s 837 | 838 | """ # variables: (tid, Class, style, desc, status) 839 | 840 | REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s""" # variables: (id, output) 841 | 842 | IMG_TMPL = r""" 843 | 显示截图 844 | 849 | """ 850 | 851 | # ------------------------------------------------------------------------ 852 | # ENDING 853 | # 854 | 855 | ENDING_TMPL = """
 
""" 856 | 857 | # -------------------- The end of the Template class ------------------- 858 | def __getattribute__(self, item): 859 | value = object.__getattribute__(self, item) 860 | if PY3K: 861 | return value 862 | else: 863 | if isinstance(value, str): 864 | return value.decode("utf-8") 865 | else: 866 | return value 867 | 868 | 869 | TestResult = unittest.TestResult 870 | 871 | 872 | class _TestResult(TestResult): 873 | # note: _TestResult is a pure representation of results. 874 | # It lacks the output and reporting ability compares to unittest._TextTestResult. 875 | 876 | def __init__(self, verbosity=1, retry=0, save_last_try=True): 877 | TestResult.__init__(self) 878 | self.stdout0 = None 879 | self.stderr0 = None 880 | self.success_count = 0 881 | self.failure_count = 0 882 | self.error_count = 0 883 | self.verbosity = verbosity 884 | 885 | # result is a list of result in 4 tuple 886 | # ( 887 | # result code (0: success; 1: fail; 2: error), 888 | # TestCase object, 889 | # Test output (byte string), 890 | # stack trace, 891 | # ) 892 | self.result = [] 893 | self.retry = retry 894 | self.trys = 0 895 | self.status = 0 896 | self.save_last_try = save_last_try 897 | self.outputBuffer = StringIO.StringIO() 898 | 899 | def startTest(self, test): 900 | test.imgs = [] 901 | TestResult.startTest(self, test) 902 | # just one buffer for both stdout and stderr 903 | self.outputBuffer.seek(0) 904 | self.outputBuffer.truncate() 905 | stdout_redirector.fp = self.outputBuffer 906 | stderr_redirector.fp = self.outputBuffer 907 | self.stdout0 = sys.stdout 908 | self.stderr0 = sys.stderr 909 | sys.stdout = stdout_redirector 910 | sys.stderr = stderr_redirector 911 | 912 | def complete_output(self): 913 | """ 914 | Disconnect output redirection and return buffer. 915 | Safe to call multiple times. 916 | """ 917 | if self.stdout0: 918 | sys.stdout = self.stdout0 919 | sys.stderr = self.stderr0 920 | self.stdout0 = None 921 | self.stderr0 = None 922 | return self.outputBuffer.getvalue() 923 | 924 | def stopTest(self, test): 925 | # Usually one of addSuccess, addError or addFailure would have been called. 926 | # But there are some path in unittest that would bypass this. 927 | # We must disconnect stdout in stopTest(), which is guaranteed to be called. 928 | if self.retry: 929 | if self.status == 1: 930 | self.trys += 1 931 | if self.trys <= self.retry: 932 | if self.save_last_try: 933 | t = self.result.pop(-1) 934 | if t[0]==1: 935 | self.failure_count-=1 936 | else: 937 | self.error_count -= 1 938 | test=copy.copy(test) 939 | sys.stderr.write("Retesting... ") 940 | sys.stderr.write(str(test)) 941 | sys.stderr.write('..%d \n' % self.trys) 942 | try: 943 | doc = test._testMethodDoc or '' 944 | except Exception: 945 | doc = '' 946 | if doc.find('_retry')!=-1: 947 | doc = doc[:doc.find('_retry')] 948 | desc ="%s_retry:%d" %(doc, self.trys) 949 | if not PY3K: 950 | if isinstance(desc, str): 951 | desc = desc.decode("utf-8") 952 | test._testMethodDoc = desc 953 | test(self) 954 | else: 955 | self.status = 0 956 | self.trys = 0 957 | self.complete_output() 958 | 959 | def addSuccess(self, test): 960 | self.success_count += 1 961 | self.status = 0 962 | TestResult.addSuccess(self, test) 963 | output = self.complete_output() 964 | self.result.append((0, test, output, '')) 965 | if self.verbosity > 1: 966 | sys.stderr.write('ok ') 967 | sys.stderr.write(str(test)) 968 | sys.stderr.write('\n') 969 | else: 970 | sys.stderr.write('.') 971 | 972 | def addError(self, test, err): 973 | self.error_count += 1 974 | self.status = 1 975 | TestResult.addError(self, test, err) 976 | _, _exc_str = self.errors[-1] 977 | output = self.complete_output() 978 | self.result.append((2, test, output, _exc_str)) 979 | if not getattr(test, "driver",""): 980 | pass 981 | else: 982 | try: 983 | driver = getattr(test, "driver") 984 | test.imgs.append(driver.get_screenshot_as_base64()) 985 | except Exception: 986 | pass 987 | if self.verbosity > 1: 988 | sys.stderr.write('E ') 989 | sys.stderr.write(str(test)) 990 | sys.stderr.write('\n') 991 | else: 992 | sys.stderr.write('E') 993 | 994 | def addFailure(self, test, err): 995 | self.failure_count += 1 996 | self.status = 1 997 | TestResult.addFailure(self, test, err) 998 | _, _exc_str = self.failures[-1] 999 | output = self.complete_output() 1000 | self.result.append((1, test, output, _exc_str)) 1001 | if not getattr(test, "driver",""): 1002 | pass 1003 | else: 1004 | try: 1005 | driver = getattr(test, "driver") 1006 | test.imgs.append(driver.get_screenshot_as_base64()) 1007 | except Exception as e: 1008 | pass 1009 | if self.verbosity > 1: 1010 | sys.stderr.write('F ') 1011 | sys.stderr.write(str(test)) 1012 | sys.stderr.write('\n') 1013 | else: 1014 | sys.stderr.write('F') 1015 | 1016 | 1017 | class HTMLTestRunner(Template_mixin): 1018 | 1019 | def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None, retry=0, save_last_try=False): 1020 | self.stream = stream 1021 | self.retry = retry 1022 | self.save_last_try = save_last_try 1023 | self.verbosity = verbosity 1024 | self.path = "" 1025 | if title is None: 1026 | self.title = self.DEFAULT_TITLE 1027 | else: 1028 | self.title = title 1029 | if description is None: 1030 | self.description = self.DEFAULT_DESCRIPTION 1031 | else: 1032 | self.description = description 1033 | 1034 | self.startTime = datetime.datetime.now() 1035 | 1036 | def run(self, test): 1037 | """Run the given test case or test suite.""" 1038 | result = _TestResult(self.verbosity) 1039 | test(result) 1040 | self.stopTime = datetime.datetime.now() 1041 | self.generateReport(test, result) 1042 | if PY3K: 1043 | # for python3 1044 | # print('\nTime Elapsed: %s' % (self.stopTime - self.startTime),file=sys.stderr) 1045 | output = '\nTime Elapsed: %s' % (self.stopTime - self.startTime) 1046 | sys.stderr.write(output) 1047 | else: 1048 | print >> sys.stderr, '\nTime Elapsed: %s' % (self.stopTime - self.startTime) 1049 | return result 1050 | 1051 | def sortResult(self, result_list): 1052 | # unittest does not seems to run in any particular order. 1053 | # Here at least we want to group them together by class. 1054 | rmap = {} 1055 | classes = [] 1056 | for n,t,o,e in result_list: 1057 | cls = t.__class__ 1058 | if cls not in rmap: 1059 | rmap[cls] = [] 1060 | classes.append(cls) 1061 | rmap[cls].append((n,t,o,e)) 1062 | r = [(cls, rmap[cls]) for cls in classes] 1063 | return r 1064 | 1065 | def getReportAttributes(self, result): 1066 | """ 1067 | Return report attributes as a list of (name, value). 1068 | Override this to add custom attributes. 1069 | """ 1070 | startTime = str(self.startTime)[:19] 1071 | duration = str(self.stopTime - self.startTime) 1072 | status = [] 1073 | if result.success_count: status.append(u'通过 %s' % result.success_count) 1074 | if result.failure_count: status.append(u'失败 %s' % result.failure_count) 1075 | if result.error_count: status.append(u'错误 %s' % result.error_count ) 1076 | if status: 1077 | status = ' '.join(status) 1078 | else: 1079 | status = 'none' 1080 | return [ 1081 | (u'开始时间', startTime), 1082 | (u'运行时长', duration), 1083 | (u'状态', status), 1084 | ] 1085 | 1086 | def mkdir_json(self): 1087 | is_exists = os.path.exists(self.path) 1088 | # 判断结果 1089 | if not is_exists: 1090 | try: 1091 | # 如果不存在则创建目录 1092 | # 创建目录操作函数 1093 | with open(self.path, "w+") as f: 1094 | f.write("var data = []") 1095 | return True 1096 | except Exception as e: 1097 | print(e) 1098 | return False 1099 | else: 1100 | return True 1101 | 1102 | def Write(self, title, heading, desc, data): 1103 | try: 1104 | with open(self.path, "r+") as f: 1105 | all_data = f.read().split(" = ", 1) 1106 | data_json = all_data[1] 1107 | data_json = eval(data_json) 1108 | if len(data_json) >= 10: 1109 | del data_json[0] 1110 | description = dict() 1111 | description["startTime"] = heading[0][1] 1112 | description["duration"] = heading[1][1] 1113 | if PY3K: 1114 | description["title"] = title 1115 | description["status"] = heading[2][1] 1116 | description["desc"] = desc 1117 | description["data"] = data 1118 | status = heading[2][1].split(" ") 1119 | for j in range(0, len(status)): 1120 | if status[j] == "通过": 1121 | description["success"] = str(status[j + 1]) 1122 | if status[j] == "失败": 1123 | description["fail"] = str(status[j + 1]) 1124 | if status[j] == "错误": 1125 | description["error"] = str(status[j + 1]) 1126 | else: 1127 | description["title"] = title.encode("utf-8") 1128 | description["status"] = heading[2][1].encode("utf-8") 1129 | description["desc"] = desc.encode("utf-8") 1130 | description["data"] = data.encode("utf-8") 1131 | status = heading[2][1].split(" ") 1132 | for j in range(0, len(status)): 1133 | if status[j] == u"通过": 1134 | description["success"] = str(status[j + 1]) 1135 | if status[j] == u"失败": 1136 | description["fail"] = str(status[j + 1]) 1137 | if status[j] == u"错误": 1138 | description["error"] = str(status[j + 1]) 1139 | data_json.append(description) 1140 | data_json = str(data_json) 1141 | f.seek(0) 1142 | f.truncate() 1143 | f.write(str("var data = " + data_json)) 1144 | except IndexError: 1145 | sys.stderr.write("JSON初始化内容有误! 初始化内容’var data = []‘") 1146 | 1147 | def generateReport(self, test, result): 1148 | report_attrs = self.getReportAttributes(result) 1149 | generator = 'HTMLTestRunner %s' % __version__ 1150 | stylesheet = self._generate_stylesheet() 1151 | heading = self._generate_heading(report_attrs) 1152 | report = self._generate_report(result) 1153 | self.path = os.path.splitext(self.stream.name)[0] + ".json" 1154 | if self.mkdir_json(): 1155 | self.Write(saxutils.escape(self.title), report_attrs, saxutils.escape(self.description), report) 1156 | ending = self._generate_ending() 1157 | chart = self._generate_chart(result) 1158 | output = self.HTML_TMPL % dict( 1159 | jsonpath = os.path.split(self.path)[1], 1160 | title = saxutils.escape(self.title), 1161 | generator = generator, 1162 | stylesheet = stylesheet, 1163 | heading = heading, 1164 | report = report, 1165 | ending = ending, 1166 | chart_script = chart 1167 | ) 1168 | if PY3K: 1169 | self.stream.write(output.encode()) 1170 | else: 1171 | self.stream.write(output.encode('utf8')) 1172 | 1173 | def _generate_stylesheet(self): 1174 | return self.STYLESHEET_TMPL 1175 | 1176 | def _generate_heading(self, report_attrs): 1177 | a_lines = [] 1178 | for name, value in report_attrs: 1179 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1180 | name = saxutils.escape(name), 1181 | value = saxutils.escape(value), 1182 | ) 1183 | a_lines.append(line) 1184 | heading = self.HEADING_TMPL % dict( 1185 | title = saxutils.escape(self.title), 1186 | parameters = ''.join(a_lines), 1187 | description = saxutils.escape(self.description) 1188 | ) 1189 | return heading 1190 | 1191 | def _generate_report(self, result): 1192 | rows = [] 1193 | sortedResult = self.sortResult(result.result) 1194 | for cid, (cls, cls_results) in enumerate(sortedResult): 1195 | # subtotal for a class 1196 | np = nf = ne = 0 1197 | for n,t,o,e in cls_results: 1198 | if n == 0: np += 1 1199 | elif n == 1: nf += 1 1200 | else: ne += 1 1201 | 1202 | # format class description 1203 | if cls.__module__ == "__main__": 1204 | name = cls.__name__ 1205 | else: 1206 | name = "%s.%s" % (cls.__module__, cls.__name__) 1207 | doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 1208 | desc = doc and '%s: %s' % (name, doc) or name 1209 | 1210 | row = self.REPORT_CLASS_TMPL % dict( 1211 | style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 1212 | desc = desc, 1213 | count = np+nf+ne, 1214 | Pass = np, 1215 | fail = nf, 1216 | error = ne, 1217 | cid = 'c%s' % (cid+1), 1218 | ) 1219 | rows.append(row) 1220 | 1221 | for tid, (n,t,o,e) in enumerate(cls_results): 1222 | self._generate_report_test(rows, cid, tid, n, t, o, e) 1223 | 1224 | report = self.REPORT_TMPL % dict( 1225 | test_list = ''.join(rows), 1226 | count = str(result.success_count+result.failure_count+result.error_count), 1227 | Pass = str(result.success_count), 1228 | fail = str(result.failure_count), 1229 | error = str(result.error_count), 1230 | passrate = str("%.2f%%" % (float(result.success_count) / 1231 | float(result.success_count + result.failure_count + result.error_count) * 100) 1232 | ), 1233 | ) 1234 | return report 1235 | 1236 | def _generate_chart(self, result): 1237 | chart = self.ECHARTS_SCRIPT % dict( 1238 | Pass=str(result.success_count), 1239 | fail=str(result.failure_count), 1240 | error=str(result.error_count), 1241 | ) 1242 | return chart 1243 | 1244 | def _generate_report_test(self, rows, cid, tid, n, t, o, e): 1245 | # e.g. 'pt1.1', 'ft1.1', etc 1246 | has_output = bool(o or e) 1247 | tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid + 1, tid + 1) 1248 | name = t.id().split('.')[-1] 1249 | if self.verbosity > 1: 1250 | doc = t._testMethodDoc or '' 1251 | else: 1252 | doc = "" 1253 | 1254 | desc = doc and ('%s: %s' % (name, doc)) or name 1255 | if not PY3K: 1256 | if isinstance(desc, str): 1257 | desc = desc.decode("utf-8") 1258 | tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 1259 | 1260 | # o and e should be byte string because they are collected from stdout and stderr? 1261 | if isinstance(o, str): 1262 | # uo = unicode(o.encode('string_escape')) 1263 | if PY3K: 1264 | uo = o 1265 | else: 1266 | uo = o.decode('utf-8', 'ignore') 1267 | else: 1268 | uo = o 1269 | if isinstance(e, str): 1270 | # ue = unicode(e.encode('string_escape')) 1271 | if PY3K: 1272 | ue = e 1273 | elif e.find("Error") != -1 or e.find("Exception") != -1: 1274 | es = e.decode('utf-8', 'ignore').split('\n') 1275 | es[-2] = es[-2].decode('unicode_escape') 1276 | ue = u"\n".join(es) 1277 | else: 1278 | ue = e.decode('utf-8', 'ignore') 1279 | else: 1280 | ue = e 1281 | 1282 | script = self.REPORT_TEST_OUTPUT_TMPL % dict( 1283 | id=tid, 1284 | output=saxutils.escape(uo + ue), 1285 | ) 1286 | if getattr(t,'imgs',[]): 1287 | # 判断截图列表,如果有则追加 1288 | tmp = u"" 1289 | for i, img in enumerate(t.imgs): 1290 | if i==0: 1291 | tmp+=""" \n""" % img 1292 | else: 1293 | tmp+=""" \n""" % img 1294 | imgs = self.IMG_TMPL % dict(imgs=tmp) 1295 | else: 1296 | imgs = u"""无截图""" 1297 | 1298 | row = tmpl % dict( 1299 | tid=tid, 1300 | Class=(n == 0 and 'hiddenRow' or 'none'), 1301 | style=n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'), 1302 | desc=desc, 1303 | script=script, 1304 | status=self.STATUS[n], 1305 | img=imgs, 1306 | ) 1307 | rows.append(row) 1308 | if not has_output: 1309 | return 1310 | 1311 | def _generate_ending(self): 1312 | return self.ENDING_TMPL 1313 | 1314 | 1315 | ############################################################################## 1316 | # Facilities for running tests from the command line 1317 | ############################################################################## 1318 | 1319 | # Note: Reuse unittest.TestProgram to launch test. In the future we may 1320 | # build our own launcher to support more specific command line 1321 | # parameters like test title, CSS, etc. 1322 | class TestProgram(unittest.TestProgram): 1323 | """ 1324 | A variation of the unittest.TestProgram. Please refer to the base 1325 | class for command line parameters. 1326 | """ 1327 | def runTests(self): 1328 | # Pick HTMLTestRunner as the default test runner. 1329 | # base class's testRunner parameter is not useful because it means 1330 | # we have to instantiate HTMLTestRunner before we know self.verbosity. 1331 | if self.testRunner is None: 1332 | self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 1333 | unittest.TestProgram.runTests(self) 1334 | 1335 | main = TestProgram 1336 | 1337 | ############################################################################## 1338 | # Executing this module from the command line 1339 | ############################################################################## 1340 | 1341 | if __name__ == "__main__": 1342 | main(module=None) 1343 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTMLTestRunner_Chart 基于unittest的测试报告,使用详情见demo 2 | 参考链接:
3 | http://tungwaiyip.info/software/HTMLTestRunner.html
4 | https://github.com/GoverSky/HTMLTestRunner_cn
5 | ### 优化报告内容 6 | 1. 测试报告中文显示,优化一些断言失败正文乱码问题
7 | 2. 新增错误和失败截图,展示到html报告里
8 | 3. 增加饼图统计
9 | 4. 失败后重试功能
10 | 5. 保存近10次测试结果,并通过柱状图展示
11 | 6. 切换测试日期,展示历史测试结果
12 | 兼容python2.x 和3.x 13 | ### 注意: 14 | 1. 在是python3.x 中,如果在这里setUp里初始化driver ,因为3.x版本 unittest 运行机制不同,会导致用力失败时截图失败,目前只有采用捕获异常来截图,或者在setUpClass里初始化driver
15 | 2. driver初始化变量名必须命名为driver 16 | ### 报告首页: 17 | ![报告截图](https://github.com/githublitao/HTMLTestRunner_Chart/blob/master/img/%E9%A6%96%E9%A1%B51.png)
18 | ### 用例截图: 19 | ![用例截图](https://github.com/githublitao/HTMLTestRunner_Chart/blob/master/img/%E6%98%BE%E7%A4%BA%E6%88%AA%E5%9B%BE1.png)
20 | ### 失败饼图: 21 | ![失败饼图](https://github.com/githublitao/HTMLTestRunner_Chart/blob/master/img/%E9%A5%BC%E5%9B%BE1.png)
22 | ### 历史走势: 23 | ![历史走势](https://github.com/githublitao/HTMLTestRunner_Chart/blob/master/img/%E8%B5%B0%E5%8A%BF%E5%9B%BE1.png)
24 | ### 微信打赏: 25 | ![微信打赏](https://github.com/githublitao/api_automation_test/blob/master/img/%E6%94%B6%E6%AC%BE%E7%A0%81.png)
26 |
27 | ### 失败重试: 28 | 1. 生成报告的参数里面加了一个参数retry=1,这个表示用例失败后,会重新跑一次。
29 | ```python 30 | if __name__ == '__main__': 31 | suite = unittest.TestLoader().loadTestsFromTestCase(case_01) 32 | runner = HTMLTestRunner( 33 | title="带截图,饼图,折线图,历史结果查看的测试报告", 34 | description="", 35 | stream=open("./demo.html", "wb"), 36 | verbosity=2, 37 | retry=0, 38 | save_last_try=True) 39 | runner.run(suite) 40 | ``` 41 | ### 保存测试结果到json文件: 42 | ```python 43 | def mkdir_json(self): 44 | is_exists = os.path.exists(self.path) 45 | # 判断结果 46 | if not is_exists: 47 | try: 48 | # 如果不存在则创建目录 49 | # 创建目录操作函数 50 | with open(self.path, "w+") as f: 51 | f.write("var data = []") 52 | return True 53 | except Exception as e: 54 | print(e) 55 | return False 56 | else: 57 | return True 58 | 59 | def Write(self, title, heading, desc, data): 60 | try: 61 | with open(self.path, "r+") as f: 62 | all_data = f.read().split(" = ", 1) 63 | data_json = all_data[1] 64 | data_json = eval(data_json) 65 | if len(data_json) >= 10: 66 | del data_json[0] 67 | description = dict() 68 | description["startTime"] = heading[0][1] 69 | description["duration"] = heading[1][1] 70 | if PY3K: 71 | description["title"] = title 72 | description["status"] = heading[2][1] 73 | description["desc"] = desc 74 | description["data"] = data 75 | status = heading[2][1].split(" ") 76 | for j in range(0, len(status)): 77 | if status[j] == "通过": 78 | description["success"] = str(status[j + 1]) 79 | if status[j] == "失败": 80 | description["fail"] = str(status[j + 1]) 81 | if status[j] == "错误": 82 | description["error"] = str(status[j + 1]) 83 | else: 84 | description["title"] = title.encode("gbk") 85 | description["status"] = heading[2][1].encode("gbk") 86 | description["desc"] = desc.encode("gbk") 87 | description["data"] = data.encode("gbk") 88 | status = heading[2][1].split(" ") 89 | for j in range(0, len(status)): 90 | if status[j] == u"通过": 91 | description["success"] = str(status[j + 1]) 92 | if status[j] == u"失败": 93 | description["fail"] = str(status[j + 1]) 94 | if status[j] == u"错误": 95 | description["error"] = str(status[j + 1]) 96 | data_json.append(description) 97 | data_json = str(data_json) 98 | f.seek(0) 99 | f.truncate() 100 | f.write(str("var data = " + data_json)) 101 | except IndexError: 102 | sys.stderr.write("JSON初始化内容有误! 初始化内容’var data = []‘") 103 | ``` 104 | ### 错误/失败截图,修改addError和addFail函数: 105 | ```python 106 | def addFailure(self, test, err): 107 | self.failure_count += 1 108 | self.status = 1 109 | TestResult.addFailure(self, test, err) 110 | _, _exc_str = self.failures[-1] 111 | output = self.complete_output() 112 | self.result.append((1, test, output, _exc_str)) 113 | if not getattr(test, "driver",""): 114 | pass 115 | else: 116 | try: 117 | driver = getattr(test, "driver") 118 | test.imgs.append(driver.get_screenshot_as_base64()) 119 | except Exception as e: 120 | pass 121 | if self.verbosity > 1: 122 | sys.stderr.write('F ') 123 | sys.stderr.write(str(test)) 124 | sys.stderr.write('\n') 125 | else: 126 | sys.stderr.write('F') 127 | ``` 128 | ### 错误重试,修改stopTest函数: 129 | ```python 130 | def stopTest(self, test): 131 | # Usually one of addSuccess, addError or addFailure would have been called. 132 | # But there are some path in unittest that would bypass this. 133 | # We must disconnect stdout in stopTest(), which is guaranteed to be called. 134 | if self.retry: 135 | if self.status == 1: 136 | self.trys += 1 137 | if self.trys <= self.retry: 138 | if self.save_last_try: 139 | t = self.result.pop(-1) 140 | if t[0]==1: 141 | self.failure_count-=1 142 | else: 143 | self.error_count -= 1 144 | test=copy.copy(test) 145 | sys.stderr.write("Retesting... ") 146 | sys.stderr.write(str(test)) 147 | sys.stderr.write('..%d \n' % self.trys) 148 | doc = test._testMethodDoc or '' 149 | if doc.find('_retry')!=-1: 150 | doc = doc[:doc.find('_retry')] 151 | desc ="%s_retry:%d" %(doc, self.trys) 152 | if not PY3K: 153 | if isinstance(desc, str): 154 | desc = desc.decode("utf-8") 155 | test._testMethodDoc = desc 156 | test(self) 157 | else: 158 | self.status = 0 159 | self.trys = 0 160 | self.complete_output() 161 | ``` 162 | ### HTML模板导入JSON历史结果,如果JSON出现错误,则历史结果和走势图错误: 163 | ```html 164 | 165 | %(title)s 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | %(stylesheet)s 174 | 175 | 176 | ``` 177 | -------------------------------------------------------------------------------- /demo.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githublitao/HTMLTestRunner_Chart/a49028b7f9dbde429ecfaf6bfe39b622fb5e5872/demo.json -------------------------------------------------------------------------------- /img/收款码1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githublitao/HTMLTestRunner_Chart/a49028b7f9dbde429ecfaf6bfe39b622fb5e5872/img/收款码1.png -------------------------------------------------------------------------------- /img/显示截图1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githublitao/HTMLTestRunner_Chart/a49028b7f9dbde429ecfaf6bfe39b622fb5e5872/img/显示截图1.png -------------------------------------------------------------------------------- /img/走势图1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githublitao/HTMLTestRunner_Chart/a49028b7f9dbde429ecfaf6bfe39b622fb5e5872/img/走势图1.png -------------------------------------------------------------------------------- /img/饼图1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githublitao/HTMLTestRunner_Chart/a49028b7f9dbde429ecfaf6bfe39b622fb5e5872/img/饼图1.png -------------------------------------------------------------------------------- /img/首页1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githublitao/HTMLTestRunner_Chart/a49028b7f9dbde429ecfaf6bfe39b622fb5e5872/img/首页1.png -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 githublitao 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 | -------------------------------------------------------------------------------- /test_selenium.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @Time    : 2018/9/14 17:52 4 | 5 | # @Author  : litao 6 | 7 | # @Desc : ============================================== 8 | 9 | # Life is Short I Use Python!!!                      === 10 | 11 | # If this runs wrong,don't ask me,I don't know why;  === 12 | 13 | # If this runs right,thank god,and I don't know why. === 14 | 15 | # Maybe the answer,my friend,is blowing in the wind. === 16 | 17 | # ====================================================== 18 | 19 | # @Project : project 20 | 21 | # @FileName: test_selenium.py 22 | 23 | # @Software: PyCharm 24 | 25 | """HTMLTestRunner 截图版示例 selenium 版""" 26 | 27 | from selenium import webdriver 28 | import unittest 29 | 30 | # from HTMLTestRunner_Chart import HTMLTestRunner 31 | from HTMLTestRunner_Chart import HTMLTestRunner 32 | 33 | 34 | class case_01(unittest.TestCase): 35 | 36 | @classmethod 37 | def setUpClass(cls): 38 | cls.driver = webdriver.Chrome() 39 | 40 | @classmethod 41 | def tearDownClass(cls): 42 | cls.driver.quit() 43 | 44 | def add_img(self): 45 | self.imgs.append(self.driver.get_screenshot_as_base64()) 46 | return True 47 | 48 | def setUp(self): 49 | # 在是python3.x 中,如果在这里初始化driver ,因为3.x版本 unittest 运行机制不同,会导致用力失败时截图失败 50 | # self.driver = webdriver.Chrome() 51 | self.imgs = [] 52 | self.addCleanup(self.cleanup) 53 | 54 | # def tearDown(self): 55 | # self.driver.quit() 56 | 57 | def cleanup(self): 58 | pass 59 | 60 | def test_case1(self): 61 | """ 百度搜索""" 62 | print("测试"*10) 63 | self.driver.get("https://www.baidu.com") 64 | self.add_img() 65 | self.driver.find_element_by_id('kw').send_keys(u'百度一下') 66 | self.add_img() 67 | self.driver.find_element_by_id('su').click() 68 | # time.sleep(1) 69 | self.add_img() 70 | # self.assertTrue(False) 71 | 72 | def test_case2(self): 73 | """163邮箱""" 74 | self.driver.get("https://mail.163.com/") 75 | # raise TypeError 76 | # self.assertTrue(False) 77 | 78 | def test_case3(self): 79 | """ 博客园""" 80 | self.driver.get("https://blog.csdn.net/qw943571775") 81 | self.imgs.append(self.driver.get_screenshot_as_base64()) 82 | # raise TypeError 83 | 84 | def test_case4(self): 85 | u""" 淘宝""" 86 | self.driver.get("http://www.taobao.com/") 87 | self.add_img() 88 | # self.assertTrue(True) 89 | # raise TypeError 90 | 91 | def test_case5(self): 92 | u""" testerhome""" 93 | self.driver.get("http://testerhome.com/") 94 | self.add_img() 95 | # raise TypeError 96 | # self.assertTrue(False) 97 | 98 | 99 | if __name__ == '__main__': 100 | suite = unittest.TestLoader().loadTestsFromTestCase(case_01) 101 | runner = HTMLTestRunner( 102 | title="带截图,饼图,折线图,历史结果查看的测试报告", 103 | description="", 104 | stream=open("./demo.html", "wb"), 105 | verbosity=2, 106 | retry=0, 107 | save_last_try=True) 108 | runner.run(suite) 109 | --------------------------------------------------------------------------------