Python2 HTMLTestRunner自动化测试报告美化

python2 的测试报告美化,需要的同学直接用

  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 The simplest way to use this is to invoke its main method. E.g.
  6     import unittest
  7     import HTMLTestRunner
  8     ... define your tests ...
  9     if __name__ == '__main__':
 10         HTMLTestRunner.main()
 11 For more customization options, instantiates a HTMLTestRunner object.
 12 HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
 13     # output to a file
 14     fp = file('my_report.html', 'wb')
 15     runner = HTMLTestRunner.HTMLTestRunner(
 16                 stream=fp,
 17                 title='My unit test',
 18                 description='This demonstrates the report output by HTMLTestRunner.'
 19                 )
 20     # Use an external stylesheet.
 21     # See the Template_mixin class for more customizable options
 22     runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
 23     # run the test
 24     runner.run(my_test_suite)
 25 ------------------------------------------------------------------------
 26 Copyright (c) 2004-2007, Wai Yip Tung
 27 All rights reserved.
 28 Redistribution and use in source and binary forms, with or without
 29 modification, are permitted provided that the following conditions are
 30 met:
 31 * Redistributions of source code must retain the above copyright notice,
 32   this list of conditions and the following disclaimer.
 33 * Redistributions in binary form must reproduce the above copyright
 34   notice, this list of conditions and the following disclaimer in the
 35   documentation and/or other materials provided with the distribution.
 36 * Neither the name Wai Yip Tung nor the names of its contributors may be
 37   used to endorse or promote products derived from this software without
 38   specific prior written permission.
 39 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 40 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 41 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 42 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 43 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 44 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 45 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 46 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 47 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 48 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 49 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 50 """
 51 
 52 # URL: http://tungwaiyip.info/software/HTMLTestRunner.html
 53 
 54 __author__ = "Wai Yip Tung,  Findyou"
 55 __version__ = "0.8.2.1"
 56 
 57 
 58 """
 59 Change History
 60 Version 0.8.2.1 -Findyou
 61 * 支持中文,汉化
 62 * 调整样式,美化(需要连入网络,使用的百度的Bootstrap.js)
 63 * 增加 通过分类显示、测试人员、通过率的展示
 64 * 优化“详细”与“收起”状态的变换
 65 * 增加返回顶部的锚点
 66 Version 0.8.2
 67 * Show output inline instead of popup window (Viorel Lupu).
 68 Version in 0.8.1
 69 * Validated XHTML (Wolfgang Borgert).
 70 * Added description of test classes and test cases.
 71 Version in 0.8.0
 72 * Define Template_mixin class for customization.
 73 * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
 74 Version in 0.7.1
 75 * Back port to Python 2.3 (Frank Horowitz).
 76 * Fix missing scroll bars in detail log (Podi).
 77 """
 78 
 79 # TODO: color stderr
 80 # TODO: simplify javascript using ,ore than 1 class in the class attribute?
 81 
 82 import datetime
 83 import StringIO
 84 import sys
 85 import time
 86 import unittest
 87 from xml.sax import saxutils
 88 import sys
 89 reload(sys)
 90 sys.setdefaultencoding('utf-8')
 91 
 92 # ------------------------------------------------------------------------
 93 # The redirectors below are used to capture output during testing. Output
 94 # sent to sys.stdout and sys.stderr are automatically captured. However
 95 # in some cases sys.stdout is already cached before HTMLTestRunner is
 96 # invoked (e.g. calling logging.basicConfig). In order to capture those
 97 # output, use the redirectors for the cached stream.
 98 #
 99 # e.g.
100 #   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
101 #   >>>
102 
103 class OutputRedirector(object):
104     """ Wrapper to redirect stdout or stderr """
105     def __init__(self, fp):
106         self.fp = fp
107 
108     def write(self, s):
109         self.fp.write(s)
110 
111     def writelines(self, lines):
112         self.fp.writelines(lines)
113 
114     def flush(self):
115         self.fp.flush()
116 
117 stdout_redirector = OutputRedirector(sys.stdout)
118 stderr_redirector = OutputRedirector(sys.stderr)
119 
120 # ----------------------------------------------------------------------
121 # Template
122 
123 class Template_mixin(object):
124     """
125     Define a HTML template for report customerization and generation.
126     Overall structure of an HTML report
127     HTML
128     +------------------------+
129     |<html>                  |
130     |  <head>                |
131     |                        |
132     |   STYLESHEET           |
133     |   +----------------+   |
134     |   |                |   |
135     |   +----------------+   |
136     |                        |
137     |  </head>               |
138     |                        |
139     |  <body>                |
140     |                        |
141     |   HEADING              |
142     |   +----------------+   |
143     |   |                |   |
144     |   +----------------+   |
145     |                        |
146     |   REPORT               |
147     |   +----------------+   |
148     |   |                |   |
149     |   +----------------+   |
150     |                        |
151     |   ENDING               |
152     |   +----------------+   |
153     |   |                |   |
154     |   +----------------+   |
155     |                        |
156     |  </body>               |
157     |</html>                 |
158     +------------------------+
159     """
160 
161     STATUS = {
162     0: '通过',
163     1: '失败',
164     2: '错误',
165     }
166 
167     DEFAULT_TITLE = '自动化测试报告'
168     DEFAULT_DESCRIPTION = ''
169     DEFAULT_TESTER='WangYingHao'
170 
171     # ------------------------------------------------------------------------
172     # HTML Template
173 
174     HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
175 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
176 <html xmlns="http://www.w3.org/1999/xhtml">
177 <head>
178     <title>%(title)s</title>
179     <meta name="generator" content="%(generator)s"/>
180     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
181     <link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
182     <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
183     <script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
184     %(stylesheet)s
185 </head>
186 <body >
187 <script language="javascript" type="text/javascript">
188 output_list = Array();
189 /*level 调整增加只显示通过用例的分类 --Findyou
190 0:Summary //all hiddenRow
191 1:Failed  //pt hiddenRow, ft none
192 2:Pass    //pt none, ft hiddenRow
193 3:All     //pt none, ft none
194 */
195 function showCase(level) {
196     trs = document.getElementsByTagName("tr");
197     for (var i = 0; i < trs.length; i++) {
198         tr = trs[i];
199         id = tr.id;
200         if (id.substr(0,2) == 'ft') {
201             if (level == 2 || level == 0 ) {
202                 tr.className = 'hiddenRow';
203             }
204             else {
205                 tr.className = '';
206             }
207         }
208         if (id.substr(0,2) == 'pt') {
209             if (level < 2) {
210                 tr.className = 'hiddenRow';
211             }
212             else {
213                 tr.className = '';
214             }
215         }
216     }
217     //加入【详细】切换文字变化 --Findyou
218     detail_class=document.getElementsByClassName('detail');
219     //console.log(detail_class.length)
220     if (level == 3) {
221         for (var i = 0; i < detail_class.length; i++){
222             detail_class[i].innerHTML="收起"
223         }
224     }
225     else{
226             for (var i = 0; i < detail_class.length; i++){
227             detail_class[i].innerHTML="详细"
228         }
229     }
230 }
231 function showClassDetail(cid, count) {
232     var id_list = Array(count);
233     var toHide = 1;
234     for (var i = 0; i < count; i++) {
235         //ID修改 点 为 下划线 -Findyou
236         tid0 = 't' + cid.substr(1) + '_' + (i+1);
237         tid = 'f' + tid0;
238         tr = document.getElementById(tid);
239         if (!tr) {
240             tid = 'p' + tid0;
241             tr = document.getElementById(tid);
242         }
243         id_list[i] = tid;
244         if (tr.className) {
245             toHide = 0;
246         }
247     }
248     for (var i = 0; i < count; i++) {
249         tid = id_list[i];
250         //修改点击无法收起的BUG,加入【详细】切换文字变化 --Findyou
251         if (toHide) {
252             document.getElementById(tid).className = 'hiddenRow';
253             document.getElementById(cid).innerText = "详细"
254         }
255         else {
256             document.getElementById(tid).className = '';
257             document.getElementById(cid).innerText = "收起"
258         }
259     }
260 }
261 function html_escape(s) {
262     s = s.replace(/&/g,'&amp;');
263     s = s.replace(/</g,'&lt;');
264     s = s.replace(/>/g,'&gt;');
265     return s;
266 }
267 </script>
268 %(heading)s
269 %(report)s
270 %(ending)s
271 </body>
272 </html>
273 """
274     # variables: (title, generator, stylesheet, heading, report, ending)
275 
276 
277     # ------------------------------------------------------------------------
278     # Stylesheet
279     #
280     # alternatively use a <link> for external style sheet, e.g.
281     #   <link rel="stylesheet" href="$url" type="text/css">
282 
283     STYLESHEET_TMPL = """
284 <style type="text/css" media="screen">
285 body        { font-family: Microsoft YaHei,Tahoma,arial,helvetica,sans-serif;padding: 20px; font-size: 80%; }
286 table       { font-size: 100%; }
287 /* -- heading ---------------------------------------------------------------------- */
288 .heading {
289     margin-top: 0ex;
290     margin-bottom: 1ex;
291 }
292 .heading .description {
293     margin-top: 4ex;
294     margin-bottom: 6ex;
295 }
296 /* -- report ------------------------------------------------------------------------ */
297 #total_row  { font-weight: bold; }
298 .passCase   { color: #5cb85c; }
299 .failCase   { color: #d9534f; font-weight: bold; }
300 .errorCase  { color: #f0ad4e; font-weight: bold; }
301 .hiddenRow  { display: none; }
302 .testcase   { margin-left: 2em; }
303 </style>
304 """
305 
306     # ------------------------------------------------------------------------
307     # Heading
308     #
309 
310     HEADING_TMPL = """<div class='heading'>
311 <h1 style="font-family: Microsoft YaHei">%(title)s</h1>
312 %(parameters)s
313 <p class='description'>%(description)s</p>
314 </div>
315 """ # variables: (title, parameters, description)
316 
317     HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s : </strong> %(value)s</p>
318 """ # variables: (name, value)
319 
320 
321 
322     # ------------------------------------------------------------------------
323     # Report
324     #
325     # 汉化,加美化效果 --Findyou
326     REPORT_TMPL = """
327 <p id='show_detail_line'>
328 <a class="btn btn-primary" href='javascript:showCase(0)'>概要{ %(passrate)s }</a>
329 <a class="btn btn-danger" href='javascript:showCase(1)'>失败{ %(fail)s }</a>
330 <a class="btn btn-success" href='javascript:showCase(2)'>通过{ %(Pass)s }</a>
331 <a class="btn btn-info" href='javascript:showCase(3)'>所有{ %(count)s }</a>
332 </p>
333 <table id='result_table' class="table table-condensed table-bordered table-hover">
334 <colgroup>
335 <col align='left' />
336 <col align='right' />
337 <col align='right' />
338 <col align='right' />
339 <col align='right' />
340 <col align='right' />
341 </colgroup>
342 <tr id='header_row' class="text-center success" style="font-weight: bold;font-size: 14px;">
343     <td>用例集/测试用例</td>
344     <td>总计</td>
345     <td>通过</td>
346     <td>失败</td>
347     <td>错误</td>
348     <td>详细</td>
349 </tr>
350 %(test_list)s
351 <tr id='total_row' class="text-center active">
352     <td>总计</td>
353     <td>%(count)s</td>
354     <td>%(Pass)s</td>
355     <td>%(fail)s</td>
356     <td>%(error)s</td>
357     <td>通过率:%(passrate)s</td>
358 </tr>
359 </table>
360 """ # variables: (test_list, count, Pass, fail, error ,passrate)
361 
362     REPORT_CLASS_TMPL = r"""
363 <tr class='%(style)s warning'>
364     <td>%(desc)s</td>
365     <td class="text-center">%(count)s</td>
366     <td class="text-center">%(Pass)s</td>
367     <td class="text-center">%(fail)s</td>
368     <td class="text-center">%(error)s</td>
369     <td class="text-center"><a href="javascript:showClassDetail('%(cid)s',%(count)s)" class="detail" id='%(cid)s'>详细</a></td>
370 </tr>
371 """ # variables: (style, desc, count, Pass, fail, error, cid)
372 
373     #失败 的样式,去掉原来JS效果,美化展示效果  -Findyou
374     REPORT_TEST_WITH_OUTPUT_TMPL = r"""
375 <tr id='%(tid)s' class='%(Class)s'>
376     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
377     <td colspan='5' align='center'>
378     <!--默认收起错误信息 -Findyou
379     <button id='btn_%(tid)s' type="button"  class="btn btn-danger btn-xs collapsed" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button>
380     <div id='div_%(tid)s' class="collapse">  -->
381     <!-- 默认展开错误信息 -Findyou -->
382     <button id='btn_%(tid)s' type="button"  class="btn btn-danger btn-xs" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button>
383     <div id='div_%(tid)s' class="collapse in">
384     <pre>
385     %(script)s
386     </pre>
387     </div>
388     </td>
389 </tr>
390 """ # variables: (tid, Class, style, desc, status)
391 
392     # 通过 的样式,加标签效果  -Findyou
393     REPORT_TEST_NO_OUTPUT_TMPL = r"""
394 <tr id='%(tid)s' class='%(Class)s'>
395     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
396     <td colspan='5' align='center'><span class="label label-success success">%(status)s</span></td>
397 </tr>
398 """ # variables: (tid, Class, style, desc, status)
399 
400     REPORT_TEST_OUTPUT_TMPL = r"""
401 %(id)s: %(output)s
402 """ # variables: (id, output)
403 
404     # ------------------------------------------------------------------------
405     # ENDING
406     #
407     # 增加返回顶部按钮  --Findyou
408     ENDING_TMPL = """<div id='ending'>&nbsp;</div>
409     <div style=" position:fixed;right:50px; bottom:30px; 20px; height:20px;cursor:pointer">
410     <a href="#"><span class="glyphicon glyphicon-eject" style = "font-size:30px;" aria-hidden="true">
411     </span></a></div>
412     """
413 
414 # -------------------- The end of the Template class -------------------
415 
416 
417 TestResult = unittest.TestResult
418 
419 class _TestResult(TestResult):
420     # note: _TestResult is a pure representation of results.
421     # It lacks the output and reporting ability compares to unittest._TextTestResult.
422 
423     def __init__(self, verbosity=1):
424         TestResult.__init__(self)
425         self.stdout0 = None
426         self.stderr0 = None
427         self.success_count = 0
428         self.failure_count = 0
429         self.error_count = 0
430         self.verbosity = verbosity
431 
432         # result is a list of result in 4 tuple
433         # (
434         #   result code (0: success; 1: fail; 2: error),
435         #   TestCase object,
436         #   Test output (byte string),
437         #   stack trace,
438         # )
439         self.result = []
440         #增加一个测试通过率 --Findyou
441         self.passrate=float(0)
442 
443 
444     def startTest(self, test):
445         TestResult.startTest(self, test)
446         # just one buffer for both stdout and stderr
447         self.outputBuffer = StringIO.StringIO()
448         stdout_redirector.fp = self.outputBuffer
449         stderr_redirector.fp = self.outputBuffer
450         self.stdout0 = sys.stdout
451         self.stderr0 = sys.stderr
452         sys.stdout = stdout_redirector
453         sys.stderr = stderr_redirector
454 
455 
456     def complete_output(self):
457         """
458         Disconnect output redirection and return buffer.
459         Safe to call multiple times.
460         """
461         if self.stdout0:
462             sys.stdout = self.stdout0
463             sys.stderr = self.stderr0
464             self.stdout0 = None
465             self.stderr0 = None
466         return self.outputBuffer.getvalue()
467 
468 
469     def stopTest(self, test):
470         # Usually one of addSuccess, addError or addFailure would have been called.
471         # But there are some path in unittest that would bypass this.
472         # We must disconnect stdout in stopTest(), which is guaranteed to be called.
473         self.complete_output()
474 
475 
476     def addSuccess(self, test):
477         self.success_count += 1
478         TestResult.addSuccess(self, test)
479         output = self.complete_output()
480         self.result.append((0, test, output, ''))
481         if self.verbosity > 1:
482             sys.stderr.write('ok ')
483             sys.stderr.write(str(test))
484             sys.stderr.write('
')
485         else:
486             sys.stderr.write('.')
487 
488     def addError(self, test, err):
489         self.error_count += 1
490         TestResult.addError(self, test, err)
491         _, _exc_str = self.errors[-1]
492         output = self.complete_output()
493         self.result.append((2, test, output, _exc_str))
494         if self.verbosity > 1:
495             sys.stderr.write('E  ')
496             sys.stderr.write(str(test))
497             sys.stderr.write('
')
498         else:
499             sys.stderr.write('E')
500 
501     def addFailure(self, test, err):
502         self.failure_count += 1
503         TestResult.addFailure(self, test, err)
504         _, _exc_str = self.failures[-1]
505         output = self.complete_output()
506         self.result.append((1, test, output, _exc_str))
507         if self.verbosity > 1:
508             sys.stderr.write('F  ')
509             sys.stderr.write(str(test))
510             sys.stderr.write('
')
511         else:
512             sys.stderr.write('F')
513 
514 
515 class HTMLTestRunner(Template_mixin):
516     """
517     """
518     def __init__(self, stream=sys.stdout, verbosity=1,title=None,description=None,tester=None):
519         self.stream = stream
520         self.verbosity = verbosity
521         if title is None:
522             self.title = self.DEFAULT_TITLE
523         else:
524             self.title = title
525         if description is None:
526             self.description = self.DEFAULT_DESCRIPTION
527         else:
528             self.description = description
529         if tester is None:
530             self.tester = self.DEFAULT_TESTER
531         else:
532             self.tester = tester
533 
534         self.startTime = datetime.datetime.now()
535 
536 
537     def run(self, test):
538         "Run the given test case or test suite."
539         result = _TestResult(self.verbosity)
540         test(result)
541         self.stopTime = datetime.datetime.now()
542         self.generateReport(test, result)
543         print >>sys.stderr, '
Time Elapsed: %s' % (self.stopTime-self.startTime)
544         return result
545 
546 
547     def sortResult(self, result_list):
548         # unittest does not seems to run in any particular order.
549         # Here at least we want to group them together by class.
550         rmap = {}
551         classes = []
552         for n,t,o,e in result_list:
553             cls = t.__class__
554             if not rmap.has_key(cls):
555                 rmap[cls] = []
556                 classes.append(cls)
557             rmap[cls].append((n,t,o,e))
558         r = [(cls, rmap[cls]) for cls in classes]
559         return r
560 
561     #替换测试结果status为通过率 --Findyou
562     def getReportAttributes(self, result):
563         """
564         Return report attributes as a list of (name, value).
565         Override this to add custom attributes.
566         """
567         startTime = str(self.startTime)[:19]
568         duration = str(self.stopTime - self.startTime)
569         status = []
570         status.append('共 %s' % (result.success_count + result.failure_count + result.error_count))
571         if result.success_count: status.append('通过 %s'    % result.success_count)
572         if result.failure_count: status.append('失败 %s' % result.failure_count)
573         if result.error_count:   status.append('错误 %s'   % result.error_count  )
574         if status:
575             status = ''.join(status)
576             self.passrate = str("%.2f%%" % (float(result.success_count) / float(result.success_count + result.failure_count + result.error_count) * 100))
577         else:
578             status = 'none'
579         return [
580             (u'测试人员', self.tester),
581             (u'开始时间',startTime),
582             (u'合计耗时',duration),
583             (u'测试结果',status + ",通过率= "+self.passrate),
584         ]
585 
586 
587     def generateReport(self, test, result):
588         report_attrs = self.getReportAttributes(result)
589         generator = 'HTMLTestRunner %s' % __version__
590         stylesheet = self._generate_stylesheet()
591         heading = self._generate_heading(report_attrs)
592         report = self._generate_report(result)
593         ending = self._generate_ending()
594         output = self.HTML_TMPL % dict(
595             title = saxutils.escape(self.title),
596             generator = generator,
597             stylesheet = stylesheet,
598             heading = heading,
599             report = report,
600             ending = ending,
601         )
602         self.stream.write(output.encode('utf8'))
603 
604 
605     def _generate_stylesheet(self):
606         return self.STYLESHEET_TMPL
607 
608     #增加Tester显示 -Findyou
609     def _generate_heading(self, report_attrs):
610         a_lines = []
611         for name, value in report_attrs:
612             line = self.HEADING_ATTRIBUTE_TMPL % dict(
613                     name = saxutils.escape(name),
614                     value = saxutils.escape(value),
615                 )
616             a_lines.append(line)
617         heading = self.HEADING_TMPL % dict(
618             title = saxutils.escape(self.title),
619             parameters = ''.join(a_lines),
620             description = saxutils.escape(self.description),
621             tester= saxutils.escape(self.tester),
622         )
623         return heading
624 
625     #生成报告  --Findyou添加注释
626     def _generate_report(self, result):
627         rows = []
628         sortedResult = self.sortResult(result.result)
629         for cid, (cls, cls_results) in enumerate(sortedResult):
630             # subtotal for a class
631             np = nf = ne = 0
632             for n,t,o,e in cls_results:
633                 if n == 0: np += 1
634                 elif n == 1: nf += 1
635                 else: ne += 1
636 
637             # format class description
638             if cls.__module__ == "__main__":
639                 name = cls.__name__
640             else:
641                 name = "%s.%s" % (cls.__module__, cls.__name__)
642             doc = cls.__doc__ and cls.__doc__.split("
")[0] or ""
643             desc = doc and '%s: %s' % (name, doc) or name
644 
645             row = self.REPORT_CLASS_TMPL % dict(
646                 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
647                 desc = desc,
648                 count = np+nf+ne,
649                 Pass = np,
650                 fail = nf,
651                 error = ne,
652                 cid = 'c%s' % (cid+1),
653             )
654             rows.append(row)
655 
656             for tid, (n,t,o,e) in enumerate(cls_results):
657                 self._generate_report_test(rows, cid, tid, n, t, o, e)
658 
659         report = self.REPORT_TMPL % dict(
660             test_list = ''.join(rows),
661             count = str(result.success_count+result.failure_count+result.error_count),
662             Pass = str(result.success_count),
663             fail = str(result.failure_count),
664             error = str(result.error_count),
665             passrate =self.passrate,
666         )
667         return report
668 
669 
670     def _generate_report_test(self, rows, cid, tid, n, t, o, e):
671         # e.g. 'pt1.1', 'ft1.1', etc
672         has_output = bool(o or e)
673         # ID修改点为下划线,支持Bootstrap折叠展开特效 - Findyou
674         tid = (n == 0 and 'p' or 'f') + 't%s_%s' % (cid+1,tid+1)
675         name = t.id().split('.')[-1]
676         doc = t.shortDescription() or ""
677         desc = doc and ('%s: %s' % (name, doc)) or name
678         tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
679 
680         # utf-8 支持中文 - Findyou
681          # o and e should be byte string because they are collected from stdout and stderr?
682         if isinstance(o, str):
683             # TODO: some problem with 'string_escape': it escape 
 and mess up formating
684             # uo = unicode(o.encode('string_escape'))
685             # uo = o.decode('latin-1')
686             uo = o.decode('utf-8')
687         else:
688             uo = o
689         if isinstance(e, str):
690             # TODO: some problem with 'string_escape': it escape 
 and mess up formating
691             # ue = unicode(e.encode('string_escape'))
692             # ue = e.decode('latin-1')
693             ue = e.decode('utf-8')
694         else:
695             ue = e
696 
697         script = self.REPORT_TEST_OUTPUT_TMPL % dict(
698             id = tid,
699             output = saxutils.escape(uo+ue),
700         )
701 
702         row = tmpl % dict(
703             tid = tid,
704             Class = (n == 0 and 'hiddenRow' or 'none'),
705             style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'),
706             desc = desc,
707             script = script,
708             status = self.STATUS[n],
709         )
710         rows.append(row)
711         if not has_output:
712             return
713 
714     def _generate_ending(self):
715         return self.ENDING_TMPL
716 
717 
718 ##############################################################################
719 # Facilities for running tests from the command line
720 ##############################################################################
721 
722 # Note: Reuse unittest.TestProgram to launch test. In the future we may
723 # build our own launcher to support more specific command line
724 # parameters like test title, CSS, etc.
725 class TestProgram(unittest.TestProgram):
726     """
727     A variation of the unittest.TestProgram. Please refer to the base
728     class for command line parameters.
729     """
730     def runTests(self):
731         # Pick HTMLTestRunner as the default test runner.
732         # base class's testRunner parameter is not useful because it means
733         # we have to instantiate HTMLTestRunner before we know self.verbosity.
734         if self.testRunner is None:
735             self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
736         unittest.TestProgram.runTests(self)
737 
738 main = TestProgram
739 
740 ##############################################################################
741 # Executing this module from the command line
742 ##############################################################################
743 
744 if __name__ == "__main__":
745     main(module=None)




效果:

Python2 HTMLTestRunner自动化测试报告美化