Python3之HTMLTestRunner测试报告美化

  前面我们讲到过在做自动化测试或单元测试的时候使用HTMLTestRunner来生成测试报告,并且由于Python2 和 Python3 对于HTMLTestRunner的支持稍微有点差异,所以我们将HTMLTestRunner进行了改造,从而适配Python3,详细改造步骤可以参考:HTMLTestRunner修改成Python3版本

  但是改造后的HTMLTestRunner生成的测试报告不是特别的美观,所以我又对HTMLTestRunner进行了进一步的改造,主要是做一些美化。

  美化之前的测试报告如下:

   美化之后的测试报告如下:

   从上面两个报告的对比来看,第二个测试报告是不是更为美观呢?

下面是我改造后的HTMLTestRunner源码:

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

 如何使用呢?

  使用方法:

    1.将上面的源码复制一份,将文件命名为HTMLTestRunner.py ,然后将HTMLTestRunner.py 文件放到Python安装目录的Lib目录下:

    2.导入HTMLTestRunner.py模块,运行以下代码,即可生成美化后的测试报告: 

 1 #!/usr/bin/env python
 2 # -*- coding: utf-8 -*-
 3 # @Time    : 2019/12/29 15:41
 4 # @Author  : lwjnicole
 5 # @File    : run_tests.py
 6 
 7 import unittest
 8 import time
 9 from HTMLTestRunner import HTMLTestRunner
10 
11 testcase_dir = './interface/configcenter'
12 discover = unittest.defaultTestLoader.discover(testcase_dir, '*_test.py')
13 
14 if __name__ == '__main__':
15     now = time.strftime("%Y-%m-%d %H_%M_%S")
16     filename = './report/' + now + '_result.html'
17     with open(filename, 'wb') as f:
18         runner = HTMLTestRunner(stream=f, title='接口自动化测试报告', description='营销线接口')
19         runner.run(discover)

  testcase_dir:指定需要执行的测试用例目录路径;

  HTMLTestRunner(stream=f, title='接口自动化测试报告', description='营销线接口'):还可以传入tester='lwjnicole',tester表示测试人员;

  执行完成之后,就会在上面自定义report目录下生成指定格式文件名的html测试报告啦!

猜你喜欢

转载自www.cnblogs.com/lwjnicole/p/12339874.html
今日推荐