unittest 报告——HTMLTestRunner/BSTestRunner+代码覆盖率

1. HTMLTestRunner.py 代码(python3)如下:

python2:  https://github.com/tungwaiyip/HTMLTestRunner

  1 """
  2 A TestRunner for use with the Python unit testing framework. It
  3 generates a HTML report to show the result at a glance.
  4 
  5 The simplest way to use this is to invoke its main method. E.g.
  6 
  7     import unittest
  8     import HTMLTestRunner
  9 
 10     ... define your tests ...
 11 
 12     if __name__ == '__main__':
 13         HTMLTestRunner.main()
 14 
 15 
 16 For more customization options, instantiates a HTMLTestRunner object.
 17 HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
 18 
 19     # output to a file
 20     fp = file('my_report.html', 'wb')
 21     runner = HTMLTestRunner.HTMLTestRunner(
 22                 stream=fp,
 23                 title='My unit test',
 24                 description='This demonstrates the report output by HTMLTestRunner.'
 25                 )
 26 
 27     # Use an external stylesheet.
 28     # See the Template_mixin class for more customizable options
 29     runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
 30 
 31     # run the test
 32     runner.run(my_test_suite)
 33 
 34 
 35 ------------------------------------------------------------------------
 36 Copyright (c) 2004-2007, Wai Yip Tung
 37 All rights reserved.
 38 
 39 Redistribution and use in source and binary forms, with or without
 40 modification, are permitted provided that the following conditions are
 41 met:
 42 
 43 * Redistributions of source code must retain the above copyright notice,
 44   this list of conditions and the following disclaimer.
 45 * Redistributions in binary form must reproduce the above copyright
 46   notice, this list of conditions and the following disclaimer in the
 47   documentation and/or other materials provided with the distribution.
 48 * Neither the name Wai Yip Tung nor the names of its contributors may be
 49   used to endorse or promote products derived from this software without
 50   specific prior written permission.
 51 
 52 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 53 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 54 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 55 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 56 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 57 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 58 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 59 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 60 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 61 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 62 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 63 """
 64 
 65 # URL: http://tungwaiyip.info/software/HTMLTestRunner.html
 66 
 67 __author__ = "Wai Yip Tung"
 68 __version__ = "0.8.3"
 69 
 70 
 71 """
 72 Change History
 73 
 74 Version 0.8.3
 75 * Prevent crash on class or module-level exceptions (Darren Wurf).
 76 
 77 Version 0.8.2
 78 * Show output inline instead of popup window (Viorel Lupu).
 79 
 80 Version in 0.8.1
 81 * Validated XHTML (Wolfgang Borgert).
 82 * Added description of test classes and test cases.
 83 
 84 Version in 0.8.0
 85 * Define Template_mixin class for customization.
 86 * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
 87 
 88 Version in 0.7.1
 89 * Back port to Python 2.3 (Frank Horowitz).
 90 * Fix missing scroll bars in detail log (Podi).
 91 """
 92 
 93 # TODO: color stderr
 94 # TODO: simplify javascript using ,ore than 1 class in the class attribute?
 95 
 96 import datetime
 97 import io
 98 import sys
 99 import time
100 import unittest
101 from xml.sax import saxutils
102 
103 
104 # ------------------------------------------------------------------------
105 # The redirectors below are used to capture output during testing. Output
106 # sent to sys.stdout and sys.stderr are automatically captured. However
107 # in some cases sys.stdout is already cached before HTMLTestRunner is
108 # invoked (e.g. calling logging.basicConfig). In order to capture those
109 # output, use the redirectors for the cached stream.
110 #
111 # e.g.
112 #   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
113 #   >>>
114 
115 def to_unicode(s):
116     try:
117         return str(s)
118     except UnicodeDecodeError:
119         # s is non ascii byte string
120         return s.decode('unicode_escape')
121 
122 class OutputRedirector(object):
123     """ Wrapper to redirect stdout or stderr """
124     def __init__(self, fp):
125         self.fp = fp
126 
127     def write(self, s):
128         self.fp.write(to_unicode(s))
129 
130     def writelines(self, lines):
131         lines = map(to_unicode, 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 
142 # ----------------------------------------------------------------------
143 # Template
144 
145 class Template_mixin(object):
146     """
147     Define a HTML template for report customerization and generation.
148 
149     Overall structure of an HTML report
150 
151     HTML
152     +------------------------+
153     |<html>                  |
154     |  <head>                |
155     |                        |
156     |   STYLESHEET           |
157     |   +----------------+   |
158     |   |                |   |
159     |   +----------------+   |
160     |                        |
161     |  </head>               |
162     |                        |
163     |  <body>                |
164     |                        |
165     |   HEADING              |
166     |   +----------------+   |
167     |   |                |   |
168     |   +----------------+   |
169     |                        |
170     |   REPORT               |
171     |   +----------------+   |
172     |   |                |   |
173     |   +----------------+   |
174     |                        |
175     |   ENDING               |
176     |   +----------------+   |
177     |   |                |   |
178     |   +----------------+   |
179     |                        |
180     |  </body>               |
181     |</html>                 |
182     +------------------------+
183     """
184 
185     STATUS = {
186     0: 'pass',
187     1: 'fail',
188     2: 'error',
189     }
190 
191     DEFAULT_TITLE = 'Unit Test Report'
192     DEFAULT_DESCRIPTION = ''
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     %(stylesheet)s
205 </head>
206 <body>
207 <script language="javascript" type="text/javascript"><!--
208 output_list = Array();
209 
210 /* level - 0:Summary; 1:Failed; 2:All */
211 function showCase(level) {
212     trs = document.getElementsByTagName("tr");
213     for (var i = 0; i < trs.length; i++) {
214         tr = trs[i];
215         id = tr.id;
216         if (id.substr(0,2) == 'ft') {
217             if (level < 1) {
218                 tr.className = 'hiddenRow';
219             }
220             else {
221                 tr.className = '';
222             }
223         }
224         if (id.substr(0,2) == 'pt') {
225             if (level > 1) {
226                 tr.className = '';
227             }
228             else {
229                 tr.className = 'hiddenRow';
230             }
231         }
232     }
233 }
234 
235 
236 function showClassDetail(cid, count) {
237     var id_list = Array(count);
238     var toHide = 1;
239     for (var i = 0; i < count; i++) {
240         tid0 = 't' + cid.substr(1) + '.' + (i+1);
241         tid = 'f' + tid0;
242         tr = document.getElementById(tid);
243         if (!tr) {
244             tid = 'p' + tid0;
245             tr = document.getElementById(tid);
246         }
247         id_list[i] = tid;
248         if (tr.className) {
249             toHide = 0;
250         }
251     }
252     for (var i = 0; i < count; i++) {
253         tid = id_list[i];
254         if (toHide) {
255             document.getElementById('div_'+tid).style.display = 'none'
256             document.getElementById(tid).className = 'hiddenRow';
257         }
258         else {
259             document.getElementById(tid).className = '';
260         }
261     }
262 }
263 
264 
265 function showTestDetail(div_id){
266     var details_div = document.getElementById(div_id)
267     var displayState = details_div.style.display
268     // alert(displayState)
269     if (displayState != 'block' ) {
270         displayState = 'block'
271         details_div.style.display = 'block'
272     }
273     else {
274         details_div.style.display = 'none'
275     }
276 }
277 
278 
279 function html_escape(s) {
280     s = s.replace(/&/g,'&amp;');
281     s = s.replace(/</g,'&lt;');
282     s = s.replace(/>/g,'&gt;');
283     return s;
284 }
285 
286 /* obsoleted by detail in <div>
287 function showOutput(id, name) {
288     var w = window.open("", //url
289                     name,
290                     "resizable,scrollbars,status,width=800,height=450");
291     d = w.document;
292     d.write("<pre>");
293     d.write(html_escape(output_list[id]));
294     d.write("\n");
295     d.write("<a href='javascript:window.close()'>close</a>\n");
296     d.write("</pre>\n");
297     d.close();
298 }
299 */
300 --></script>
301 
302 %(heading)s
303 %(report)s
304 %(ending)s
305 
306 </body>
307 </html>
308 """
309     # variables: (title, generator, stylesheet, heading, report, ending)
310 
311 
312     # ------------------------------------------------------------------------
313     # Stylesheet
314     #
315     # alternatively use a <link> for external style sheet, e.g.
316     #   <link rel="stylesheet" href="$url" type="text/css">
317 
318     STYLESHEET_TMPL = """
319 <style type="text/css" media="screen">
320 body        { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
321 table       { font-size: 100%; }
322 pre         { }
323 
324 /* -- heading ---------------------------------------------------------------------- */
325 h1 {
326     font-size: 16pt;
327     color: gray;
328 }
329 .heading {
330     margin-top: 0ex;
331     margin-bottom: 1ex;
332 }
333 
334 .heading .attribute {
335     margin-top: 1ex;
336     margin-bottom: 0;
337 }
338 
339 .heading .description {
340     margin-top: 4ex;
341     margin-bottom: 6ex;
342 }
343 
344 /* -- css div popup ------------------------------------------------------------------------ */
345 a.popup_link {
346 }
347 
348 a.popup_link:hover {
349     color: red;
350 }
351 
352 .popup_window {
353     display: none;
354     position: relative;
355     left: 0px;
356     top: 0px;
357     /*border: solid #627173 1px; */
358     padding: 10px;
359     background-color: #E6E6D6;
360     font-family: "Lucida Console", "Courier New", Courier, monospace;
361     text-align: left;
362     font-size: 8pt;
363     width: 500px;
364 }
365 
366 }
367 /* -- report ------------------------------------------------------------------------ */
368 #show_detail_line {
369     margin-top: 3ex;
370     margin-bottom: 1ex;
371 }
372 #result_table {
373     width: 80%;
374     border-collapse: collapse;
375     border: 1px solid #777;
376 }
377 #header_row {
378     font-weight: bold;
379     color: white;
380     background-color: #777;
381 }
382 #result_table td {
383     border: 1px solid #777;
384     padding: 2px;
385 }
386 #total_row  { font-weight: bold; }
387 .passClass  { background-color: #6c6; }
388 .failClass  { background-color: #c60; }
389 .errorClass { background-color: #c00; }
390 .passCase   { color: #6c6; }
391 .failCase   { color: #c60; font-weight: bold; }
392 .errorCase  { color: #c00; font-weight: bold; }
393 .hiddenRow  { display: none; }
394 .testcase   { margin-left: 2em; }
395 
396 
397 /* -- ending ---------------------------------------------------------------------- */
398 #ending {
399 }
400 
401 </style>
402 """
403 
404 
405 
406     # ------------------------------------------------------------------------
407     # Heading
408     #
409 
410     HEADING_TMPL = """<div class='heading'>
411 <h1>%(title)s</h1>
412 %(parameters)s
413 <p class='description'>%(description)s</p>
414 </div>
415 
416 """ # variables: (title, parameters, description)
417 
418     HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
419 """ # variables: (name, value)
420 
421 
422 
423     # ------------------------------------------------------------------------
424     # Report
425     #
426 
427     REPORT_TMPL = """
428 <p id='show_detail_line'>Show
429 <a href='javascript:showCase(0)'>Summary</a>
430 <a href='javascript:showCase(1)'>Failed</a>
431 <a href='javascript:showCase(2)'>All</a>
432 </p>
433 <table id='result_table'>
434 <colgroup>
435 <col align='left' />
436 <col align='right' />
437 <col align='right' />
438 <col align='right' />
439 <col align='right' />
440 <col align='right' />
441 </colgroup>
442 <tr id='header_row'>
443     <td>Test Group/Test case</td>
444     <td>Count</td>
445     <td>Pass</td>
446     <td>Fail</td>
447     <td>Error</td>
448     <td>View</td>
449 </tr>
450 %(test_list)s
451 <tr id='total_row'>
452     <td>Total</td>
453     <td>%(count)s</td>
454     <td>%(Pass)s</td>
455     <td>%(fail)s</td>
456     <td>%(error)s</td>
457     <td>&nbsp;</td>
458 </tr>
459 </table>
460 """ # variables: (test_list, count, Pass, fail, error)
461 
462     REPORT_CLASS_TMPL = r"""
463 <tr class='%(style)s'>
464     <td>%(desc)s</td>
465     <td>%(count)s</td>
466     <td>%(Pass)s</td>
467     <td>%(fail)s</td>
468     <td>%(error)s</td>
469     <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
470 </tr>
471 """ # variables: (style, desc, count, Pass, fail, error, cid)
472 
473 
474     REPORT_TEST_WITH_OUTPUT_TMPL = r"""
475 <tr id='%(tid)s' class='%(Class)s'>
476     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
477     <td colspan='5' align='center'>
478 
479     <!--css div popup start-->
480     <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
481         %(status)s</a>
482 
483     <div id='div_%(tid)s' class="popup_window">
484         <div style='text-align: right; color:red;cursor:pointer'>
485         <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
486            [x]</a>
487         </div>
488         <pre>
489         %(script)s
490         </pre>
491     </div>
492     <!--css div popup end-->
493 
494     </td>
495 </tr>
496 """ # variables: (tid, Class, style, desc, status)
497 
498 
499     REPORT_TEST_NO_OUTPUT_TMPL = r"""
500 <tr id='%(tid)s' class='%(Class)s'>
501     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
502     <td colspan='5' align='center'>%(status)s</td>
503 </tr>
504 """ # variables: (tid, Class, style, desc, status)
505 
506 
507     REPORT_TEST_OUTPUT_TMPL = r"""
508 %(id)s: %(output)s
509 """ # variables: (id, output)
510 
511 
512 
513     # ------------------------------------------------------------------------
514     # ENDING
515     #
516 
517     ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
518 
519 # -------------------- The end of the Template class -------------------
520 
521 
522 TestResult = unittest.TestResult
523 
524 class _TestResult(TestResult):
525     # note: _TestResult is a pure representation of results.
526     # It lacks the output and reporting ability compares to unittest._TextTestResult.
527 
528     def __init__(self, verbosity=1):
529         TestResult.__init__(self)
530         self.outputBuffer = io.StringIO()
531         self.stdout0 = None
532         self.stderr0 = None
533         self.success_count = 0
534         self.failure_count = 0
535         self.error_count = 0
536         self.verbosity = verbosity
537 
538         # result is a list of result in 4 tuple
539         # (
540         #   result code (0: success; 1: fail; 2: error),
541         #   TestCase object,
542         #   Test output (byte string),
543         #   stack trace,
544         # )
545         self.result = []
546 
547 
548     def startTest(self, test):
549         TestResult.startTest(self, test)
550         # just one buffer for both stdout and stderr
551         stdout_redirector.fp = self.outputBuffer
552         stderr_redirector.fp = self.outputBuffer
553         self.stdout0 = sys.stdout
554         self.stderr0 = sys.stderr
555         sys.stdout = stdout_redirector
556         sys.stderr = stderr_redirector
557 
558 
559     def complete_output(self):
560         """
561         Disconnect output redirection and return buffer.
562         Safe to call multiple times.
563         """
564         if self.stdout0:
565             sys.stdout = self.stdout0
566             sys.stderr = self.stderr0
567             self.stdout0 = None
568             self.stderr0 = None
569         return self.outputBuffer.getvalue()
570 
571 
572     def stopTest(self, test):
573         # Usually one of addSuccess, addError or addFailure would have been called.
574         # But there are some path in unittest that would bypass this.
575         # We must disconnect stdout in stopTest(), which is guaranteed to be called.
576         self.complete_output()
577 
578 
579     def addSuccess(self, test):
580         self.success_count += 1
581         TestResult.addSuccess(self, test)
582         output = self.complete_output()
583         self.result.append((0, test, output, ''))
584         if self.verbosity > 1:
585             sys.stderr.write('ok ')
586             sys.stderr.write(str(test))
587             sys.stderr.write('\n')
588         else:
589             sys.stderr.write('.')
590 
591     def addError(self, test, err):
592         self.error_count += 1
593         TestResult.addError(self, test, err)
594         _, _exc_str = self.errors[-1]
595         output = self.complete_output()
596         self.result.append((2, test, output, _exc_str))
597         if self.verbosity > 1:
598             sys.stderr.write('E  ')
599             sys.stderr.write(str(test))
600             sys.stderr.write('\n')
601         else:
602             sys.stderr.write('E')
603 
604     def addFailure(self, test, err):
605         self.failure_count += 1
606         TestResult.addFailure(self, test, err)
607         _, _exc_str = self.failures[-1]
608         output = self.complete_output()
609         self.result.append((1, test, output, _exc_str))
610         if self.verbosity > 1:
611             sys.stderr.write('F  ')
612             sys.stderr.write(str(test))
613             sys.stderr.write('\n')
614         else:
615             sys.stderr.write('F')
616 
617 
618 class HTMLTestRunner(Template_mixin):
619     """
620     """
621     def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
622         self.stream = stream
623         self.verbosity = verbosity
624         if title is None:
625             self.title = self.DEFAULT_TITLE
626         else:
627             self.title = title
628         if description is None:
629             self.description = self.DEFAULT_DESCRIPTION
630         else:
631             self.description = description
632 
633         self.startTime = datetime.datetime.now()
634 
635 
636     def run(self, test):
637         "Run the given test case or test suite."
638         result = _TestResult(self.verbosity)
639         test(result)
640         self.stopTime = datetime.datetime.now()
641         self.generateReport(test, result)
642         print(sys.stderr, '\nTime Elapsed: %s' % (self.stopTime - self.startTime))
643         # print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
644         return result
645 
646 
647     def sortResult(self, result_list):
648         # unittest does not seems to run in any particular order.
649         # Here at least we want to group them together by class.
650         rmap = {}
651         classes = []
652         for n,t,o,e in result_list:
653             cls = t.__class__
654             if not cls in rmap:
655                 rmap[cls] = []
656                 classes.append(cls)
657             rmap[cls].append((n,t,o,e))
658         r = [(cls, rmap[cls]) for cls in classes]
659         return r
660 
661 
662     def getReportAttributes(self, result):
663         """
664         Return report attributes as a list of (name, value).
665         Override this to add custom attributes.
666         """
667         startTime = str(self.startTime)[:19]
668         duration = str(self.stopTime - self.startTime)
669         status = []
670         if result.success_count: status.append('Pass %s'    % result.success_count)
671         if result.failure_count: status.append('Failure %s' % result.failure_count)
672         if result.error_count:   status.append('Error %s'   % result.error_count  )
673         if status:
674             status = ' '.join(status)
675         else:
676             status = 'none'
677         return [
678             ('Start Time', startTime),
679             ('Duration', duration),
680             ('Status', status),
681         ]
682 
683 
684     def generateReport(self, test, result):
685         report_attrs = self.getReportAttributes(result)
686         generator = 'HTMLTestRunner %s' % __version__
687         stylesheet = self._generate_stylesheet()
688         heading = self._generate_heading(report_attrs)
689         report = self._generate_report(result)
690         ending = self._generate_ending()
691         output = self.HTML_TMPL % dict(
692             title = saxutils.escape(self.title),
693             generator = generator,
694             stylesheet = stylesheet,
695             heading = heading,
696             report = report,
697             ending = ending,
698         )
699         self.stream.write(output.encode('utf8'))
700 
701 
702     def _generate_stylesheet(self):
703         return self.STYLESHEET_TMPL
704 
705 
706     def _generate_heading(self, report_attrs):
707         a_lines = []
708         for name, value in report_attrs:
709             line = self.HEADING_ATTRIBUTE_TMPL % dict(
710                     name = saxutils.escape(name),
711                     value = saxutils.escape(value),
712                 )
713             a_lines.append(line)
714         heading = self.HEADING_TMPL % dict(
715             title = saxutils.escape(self.title),
716             parameters = ''.join(a_lines),
717             description = saxutils.escape(self.description),
718         )
719         return heading
720 
721 
722     def _generate_report(self, result):
723         rows = []
724         sortedResult = self.sortResult(result.result)
725         for cid, (cls, cls_results) in enumerate(sortedResult):
726             # subtotal for a class
727             np = nf = ne = 0
728             for n,t,o,e in cls_results:
729                 if n == 0: np += 1
730                 elif n == 1: nf += 1
731                 else: ne += 1
732 
733             # format class description
734             if cls.__module__ == "__main__":
735                 name = cls.__name__
736             else:
737                 name = "%s.%s" % (cls.__module__, cls.__name__)
738             doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
739             desc = doc and '%s: %s' % (name, doc) or name
740 
741             row = self.REPORT_CLASS_TMPL % dict(
742                 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
743                 desc = desc,
744                 count = np+nf+ne,
745                 Pass = np,
746                 fail = nf,
747                 error = ne,
748                 cid = 'c%s' % (cid+1),
749             )
750             rows.append(row)
751 
752             for tid, (n,t,o,e) in enumerate(cls_results):
753                 self._generate_report_test(rows, cid, tid, n, t, o, e)
754 
755         report = self.REPORT_TMPL % dict(
756             test_list = ''.join(rows),
757             count = str(result.success_count+result.failure_count+result.error_count),
758             Pass = str(result.success_count),
759             fail = str(result.failure_count),
760             error = str(result.error_count),
761         )
762         return report
763 
764 
765     def _generate_report_test(self, rows, cid, tid, n, t, o, e):
766         # e.g. 'pt1.1', 'ft1.1', etc
767         has_output = bool(o or e)
768         tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
769         name = t.id().split('.')[-1]
770         doc = t.shortDescription() or ""
771         desc = doc and ('%s: %s' % (name, doc)) or name
772         tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
773 
774         # o and e should be byte string because they are collected from stdout and stderr?
775         if isinstance(o,str):
776             # TODO: some problem with 'string_escape': it escape \n and mess up formating
777             # uo = unicode(o.encode('string_escape'))
778             uo = e
779         else:
780             uo = o
781         if isinstance(e,str):
782             # TODO: some problem with 'string_escape': it escape \n and mess up formating
783             # ue = unicode(e.encode('string_escape'))
784             ue = e
785         else:
786             ue = e
787 
788         script = self.REPORT_TEST_OUTPUT_TMPL % dict(
789             id = tid,
790             output = saxutils.escape(uo+ue),
791         )
792 
793         row = tmpl % dict(
794             tid = tid,
795             Class = (n == 0 and 'hiddenRow' or 'none'),
796             style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
797             desc = desc,
798             script = script,
799             status = self.STATUS[n],
800         )
801         rows.append(row)
802         if not has_output:
803             return
804 
805     def _generate_ending(self):
806         return self.ENDING_TMPL
807 
808 
809 ##############################################################################
810 # Facilities for running tests from the command line
811 ##############################################################################
812 
813 # Note: Reuse unittest.TestProgram to launch test. In the future we may
814 # build our own launcher to support more specific command line
815 # parameters like test title, CSS, etc.
816 class TestProgram(unittest.TestProgram):
817     """
818     A variation of the unittest.TestProgram. Please refer to the base
819     class for command line parameters.
820     """
821     def runTests(self):
822         # Pick HTMLTestRunner as the default test runner.
823         # base class's testRunner parameter is not useful because it means
824         # we have to instantiate HTMLTestRunner before we know self.verbosity.
825         if self.testRunner is None:
826             self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
827         unittest.TestProgram.runTests(self)
828 
829 main = TestProgram
830 
831 ##############################################################################
832 # Executing this module from the command line
833 ##############################################################################
834 
835 if __name__ == "__main__":
836     main(module=None)
View Code

 

2. BSTestRunner.py 代码(python3)如下:

  1 """
  2 A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance.
  3 
  4 The simplest way to use this is to invoke its main method. E.g.
  5 
  6     import unittest
  7     import BSTestRunner
  8 
  9     ... define your tests ...
 10 
 11     if __name__ == '__main__':
 12         BSTestRunner.main()
 13 
 14 
 15 For more customization options, instantiates a BSTestRunner object.
 16 BSTestRunner is a counterpart to unittest's TextTestRunner. E.g.
 17 
 18     # output to a file
 19     fp = file('my_report.html', 'wb')
 20     runner = BSTestRunner.BSTestRunner(
 21                 stream=fp,
 22                 title='My unit test',
 23                 description='This demonstrates the report output by BSTestRunner.'
 24                 )
 25 
 26     # Use an external stylesheet.
 27     # See the Template_mixin class for more customizable options
 28     runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
 29 
 30     # run the test
 31     runner.run(my_test_suite)
 32 
 33 
 34 ------------------------------------------------------------------------
 35 Copyright (c) 2004-2007, Wai Yip Tung
 36 Copyright (c) 2016, Eason Han
 37 All rights reserved.
 38 
 39 Redistribution and use in source and binary forms, with or without
 40 modification, are permitted provided that the following conditions are
 41 met:
 42 
 43 * Redistributions of source code must retain the above copyright notice,
 44   this list of conditions and the following disclaimer.
 45 * Redistributions in binary form must reproduce the above copyright
 46   notice, this list of conditions and the following disclaimer in the
 47   documentation and/or other materials provided with the distribution.
 48 * Neither the name Wai Yip Tung nor the names of its contributors may be
 49   used to endorse or promote products derived from this software without
 50   specific prior written permission.
 51 
 52 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 53 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 54 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 55 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 56 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 57 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 58 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 59 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 60 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 61 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 62 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 63 """
 64 
 65 
 66 __author__ = "Wai Yip Tung && Eason Han"
 67 __version__ = "0.8.4"
 68 
 69 
 70 """
 71 Change History
 72 
 73 Version 0.8.3
 74 * Modify html style using bootstrap3.
 75 
 76 Version 0.8.3
 77 * Prevent crash on class or module-level exceptions (Darren Wurf).
 78 
 79 Version 0.8.2
 80 * Show output inline instead of popup window (Viorel Lupu).
 81 
 82 Version in 0.8.1
 83 * Validated XHTML (Wolfgang Borgert).
 84 * Added description of test classes and test cases.
 85 
 86 Version in 0.8.0
 87 * Define Template_mixin class for customization.
 88 * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
 89 
 90 Version in 0.7.1
 91 * Back port to Python 2.3 (Frank Horowitz).
 92 * Fix missing scroll bars in detail log (Podi).
 93 """
 94 
 95 # TODO: color stderr
 96 # TODO: simplify javascript using ,ore than 1 class in the class attribute?
 97 
 98 import datetime
 99 try:
100     from StringIO import StringIO
101 except ImportError:
102     from io import StringIO
103 import sys
104 import time
105 import unittest
106 from xml.sax import saxutils
107 
108 
109 # ------------------------------------------------------------------------
110 # The redirectors below are used to capture output during testing. Output
111 # sent to sys.stdout and sys.stderr are automatically captured. However
112 # in some cases sys.stdout is already cached before BSTestRunner is
113 # invoked (e.g. calling logging.basicConfig). In order to capture those
114 # output, use the redirectors for the cached stream.
115 #
116 # e.g.
117 #   >>> logging.basicConfig(stream=BSTestRunner.stdout_redirector)
118 #   >>>
119 
120 def to_unicode(s):
121     try:
122         return str(s)
123     except UnicodeDecodeError:
124         # s is non ascii byte string
125         return s.decode('unicode_escape')
126 
127 class OutputRedirector(object):
128     """ Wrapper to redirect stdout or stderr """
129     def __init__(self, fp):
130         self.fp = fp
131 
132     def write(self, s):
133         self.fp.write(to_unicode(s))
134 
135     def writelines(self, lines):
136         lines = map(to_unicode, lines)
137         self.fp.writelines(lines)
138 
139     def flush(self):
140         self.fp.flush()
141 
142 stdout_redirector = OutputRedirector(sys.stdout)
143 stderr_redirector = OutputRedirector(sys.stderr)
144 
145 
146 
147 # ----------------------------------------------------------------------
148 # Template
149 
150 class Template_mixin(object):
151     """
152     Define a HTML template for report customerization and generation.
153 
154     Overall structure of an HTML report
155 
156     HTML
157     +------------------------+
158     |<html>                  |
159     |  <head>                |
160     |                        |
161     |   STYLESHEET           |
162     |   +----------------+   |
163     |   |                |   |
164     |   +----------------+   |
165     |                        |
166     |  </head>               |
167     |                        |
168     |  <body>                |
169     |                        |
170     |   HEADING              |
171     |   +----------------+   |
172     |   |                |   |
173     |   +----------------+   |
174     |                        |
175     |   REPORT               |
176     |   +----------------+   |
177     |   |                |   |
178     |   +----------------+   |
179     |                        |
180     |   ENDING               |
181     |   +----------------+   |
182     |   |                |   |
183     |   +----------------+   |
184     |                        |
185     |  </body>               |
186     |</html>                 |
187     +------------------------+
188     """
189 
190     STATUS = {
191     0: 'pass',
192     1: 'fail',
193     2: 'error',
194     }
195 
196     DEFAULT_TITLE = 'Unit Test Report'
197     DEFAULT_DESCRIPTION = ''
198 
199     # ------------------------------------------------------------------------
200     # HTML Template
201 
202     HTML_TMPL = r"""<!DOCTYPE html>
203 <html lang="zh-cn">
204   <head>
205     <meta charset="utf-8">
206     <meta http-equiv="X-UA-Compatible" content="IE=edge">
207     <meta name="viewport" content="width=device-width, initial-scale=1">
208     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
209     <title>%(title)s</title>
210     <meta name="generator" content="%(generator)s"/>
211     <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css">
212     %(stylesheet)s
213 
214     <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
215     <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
216     <!--[if lt IE 9]>
217       <script src="http://cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script>
218       <script src="http://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
219     <![endif]-->
220   </head>
221 <body>
222 <script language="javascript" type="text/javascript"><!--
223 output_list = Array();
224 
225 /* level - 0:Summary; 1:Failed; 2:All */
226 function showCase(level) {
227     trs = document.getElementsByTagName("tr");
228     for (var i = 0; i < trs.length; i++) {
229         tr = trs[i];
230         id = tr.id;
231         if (id.substr(0,2) == 'ft') {
232             if (level < 1) {
233                 tr.className = 'hiddenRow';
234             }
235             else {
236                 tr.className = '';
237             }
238         }
239         if (id.substr(0,2) == 'pt') {
240             if (level > 1) {
241                 tr.className = '';
242             }
243             else {
244                 tr.className = 'hiddenRow';
245             }
246         }
247     }
248 }
249 
250 
251 function showClassDetail(cid, count) {
252     var id_list = Array(count);
253     var toHide = 1;
254     for (var i = 0; i < count; i++) {
255         tid0 = 't' + cid.substr(1) + '.' + (i+1);
256         tid = 'f' + tid0;
257         tr = document.getElementById(tid);
258         if (!tr) {
259             tid = 'p' + tid0;
260             tr = document.getElementById(tid);
261         }
262         id_list[i] = tid;
263         if (tr.className) {
264             toHide = 0;
265         }
266     }
267     for (var i = 0; i < count; i++) {
268         tid = id_list[i];
269         if (toHide) {
270             document.getElementById('div_'+tid).style.display = 'none'
271             document.getElementById(tid).className = 'hiddenRow';
272         }
273         else {
274             document.getElementById(tid).className = '';
275         }
276     }
277 }
278 
279 
280 function showTestDetail(div_id){
281     var details_div = document.getElementById(div_id)
282     var displayState = details_div.style.display
283     // alert(displayState)
284     if (displayState != 'block' ) {
285         displayState = 'block'
286         details_div.style.display = 'block'
287     }
288     else {
289         details_div.style.display = 'none'
290     }
291 }
292 
293 
294 function html_escape(s) {
295     s = s.replace(/&/g,'&amp;');
296     s = s.replace(/</g,'&lt;');
297     s = s.replace(/>/g,'&gt;');
298     return s;
299 }
300 
301 /* obsoleted by detail in <div>
302 function showOutput(id, name) {
303     var w = window.open("", //url
304                     name,
305                     "resizable,scrollbars,status,width=800,height=450");
306     d = w.document;
307     d.write("<pre>");
308     d.write(html_escape(output_list[id]));
309     d.write("\n");
310     d.write("<a href='javascript:window.close()'>close</a>\n");
311     d.write("</pre>\n");
312     d.close();
313 }
314 */
315 --></script>
316 
317 <div class="container">
318     %(heading)s
319     %(report)s
320     %(ending)s
321 </div>
322 
323 </body>
324 </html>
325 """
326     # variables: (title, generator, stylesheet, heading, report, ending)
327 
328 
329     # ------------------------------------------------------------------------
330     # Stylesheet
331     #
332     # alternatively use a <link> for external style sheet, e.g.
333     #   <link rel="stylesheet" href="$url" type="text/css">
334 
335     STYLESHEET_TMPL = """
336 <style type="text/css" media="screen">
337 
338 /* -- css div popup ------------------------------------------------------------------------ */
339 .popup_window {
340     display: none;
341     position: relative;
342     left: 0px;
343     top: 0px;
344     /*border: solid #627173 1px; */
345     padding: 10px;
346     background-color: #99CCFF;
347     font-family: "Lucida Console", "Courier New", Courier, monospace;
348     text-align: left;
349     font-size: 10pt;
350     width: 500px;
351 }
352 
353 /* -- report ------------------------------------------------------------------------ */
354 
355 #show_detail_line .label {
356     font-size: 85%;
357     cursor: pointer;
358 }
359 
360 #show_detail_line {
361     margin: 2em auto 1em auto;
362 }
363 
364 #total_row  { font-weight: bold; }
365 .hiddenRow  { display: none; }
366 .testcase   { margin-left: 2em; }
367 
368 </style>
369 """
370 
371 
372 
373     # ------------------------------------------------------------------------
374     # Heading
375     #
376 
377     HEADING_TMPL = """<div class='heading'>
378 <h1>%(title)s</h1>
379 %(parameters)s
380 <p class='description'>%(description)s</p>
381 </div>
382 
383 """ # variables: (title, parameters, description)
384 
385     HEADING_ATTRIBUTE_TMPL = """<p><strong>%(name)s:</strong> %(value)s</p>
386 """ # variables: (name, value)
387 
388 
389 
390     # ------------------------------------------------------------------------
391     # Report
392     #
393 
394     REPORT_TMPL = """
395 <p id='show_detail_line'>
396 <span class="label label-primary" onclick="showCase(0)">Summary</span>
397 <span class="label label-danger" onclick="showCase(1)">Failed</span>
398 <span class="label label-default" onclick="showCase(2)">All</span>
399 </p>
400 <table id='result_table' class="table">
401     <thead>
402         <tr id='header_row'>
403             <th>Test Group/Test case</td>
404             <th>Count</td>
405             <th>Pass</td>
406             <th>Fail</td>
407             <th>Error</td>
408             <th>View</td>
409         </tr>
410     </thead>
411     <tbody>
412         %(test_list)s
413     </tbody>
414     <tfoot>
415         <tr id='total_row'>
416             <td>Total</td>
417             <td>%(count)s</td>
418             <td class="text text-success">%(Pass)s</td>
419             <td class="text text-danger">%(fail)s</td>
420             <td class="text text-warning">%(error)s</td>
421             <td>&nbsp;</td>
422         </tr>
423     </tfoot>
424 </table>
425 """ # variables: (test_list, count, Pass, fail, error)
426 
427     REPORT_CLASS_TMPL = r"""
428 <tr class='%(style)s'>
429     <td>%(desc)s</td>
430     <td>%(count)s</td>
431     <td>%(Pass)s</td>
432     <td>%(fail)s</td>
433     <td>%(error)s</td>
434     <td><a class="btn btn-xs btn-primary"href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
435 </tr>
436 """ # variables: (style, desc, count, Pass, fail, error, cid)
437 
438 
439     REPORT_TEST_WITH_OUTPUT_TMPL = r"""
440 <tr id='%(tid)s' class='%(Class)s'>
441     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
442     <td colspan='5' align='center'>
443 
444     <!--css div popup start-->
445     <a class="popup_link btn btn-xs btn-default" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
446         %(status)s</a>
447 
448     <div id='div_%(tid)s' class="popup_window">
449         <div style='text-align: right;cursor:pointer'>
450         <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
451            [x]</a>
452         </div>
453         <pre>
454         %(script)s
455         </pre>
456     </div>
457     <!--css div popup end-->
458 
459     </td>
460 </tr>
461 """ # variables: (tid, Class, style, desc, status)
462 
463 
464     REPORT_TEST_NO_OUTPUT_TMPL = r"""
465 <tr id='%(tid)s' class='%(Class)s'>
466     <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
467     <td colspan='5' align='center'>%(status)s</td>
468 </tr>
469 """ # variables: (tid, Class, style, desc, status)
470 
471 
472     REPORT_TEST_OUTPUT_TMPL = r"""
473 %(id)s: %(output)s
474 """ # variables: (id, output)
475 
476 
477 
478     # ------------------------------------------------------------------------
479     # ENDING
480     #
481 
482     ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
483 
484 # -------------------- The end of the Template class -------------------
485 
486 
487 TestResult = unittest.TestResult
488 
489 class _TestResult(TestResult):
490     # note: _TestResult is a pure representation of results.
491     # It lacks the output and reporting ability compares to unittest._TextTestResult.
492 
493     def __init__(self, verbosity=1):
494         TestResult.__init__(self)
495         self.outputBuffer = StringIO()
496         self.stdout0 = None
497         self.stderr0 = None
498         self.success_count = 0
499         self.failure_count = 0
500         self.error_count = 0
501         self.verbosity = verbosity
502 
503         # result is a list of result in 4 tuple
504         # (
505         #   result code (0: success; 1: fail; 2: error),
506         #   TestCase object,
507         #   Test output (byte string),
508         #   stack trace,
509         # )
510         self.result = []
511 
512 
513     def startTest(self, test):
514         TestResult.startTest(self, test)
515         # just one buffer for both stdout and stderr
516         stdout_redirector.fp = self.outputBuffer
517         stderr_redirector.fp = self.outputBuffer
518         self.stdout0 = sys.stdout
519         self.stderr0 = sys.stderr
520         sys.stdout = stdout_redirector
521         sys.stderr = stderr_redirector
522 
523 
524     def complete_output(self):
525         """
526         Disconnect output redirection and return buffer.
527         Safe to call multiple times.
528         """
529         if self.stdout0:
530             sys.stdout = self.stdout0
531             sys.stderr = self.stderr0
532             self.stdout0 = None
533             self.stderr0 = None
534         return self.outputBuffer.getvalue()
535 
536 
537     def stopTest(self, test):
538         # Usually one of addSuccess, addError or addFailure would have been called.
539         # But there are some path in unittest that would bypass this.
540         # We must disconnect stdout in stopTest(), which is guaranteed to be called.
541         self.complete_output()
542 
543 
544     def addSuccess(self, test):
545         self.success_count += 1
546         TestResult.addSuccess(self, test)
547         output = self.complete_output()
548         self.result.append((0, test, output, ''))
549         if self.verbosity > 1:
550             sys.stderr.write('ok ')
551             sys.stderr.write(str(test))
552             sys.stderr.write('\n')
553         else:
554             sys.stderr.write('.')
555 
556     def addError(self, test, err):
557         self.error_count += 1
558         TestResult.addError(self, test, err)
559         _, _exc_str = self.errors[-1]
560         output = self.complete_output()
561         self.result.append((2, test, output, _exc_str))
562         if self.verbosity > 1:
563             sys.stderr.write('E  ')
564             sys.stderr.write(str(test))
565             sys.stderr.write('\n')
566         else:
567             sys.stderr.write('E')
568 
569     def addFailure(self, test, err):
570         self.failure_count += 1
571         TestResult.addFailure(self, test, err)
572         _, _exc_str = self.failures[-1]
573         output = self.complete_output()
574         self.result.append((1, test, output, _exc_str))
575         if self.verbosity > 1:
576             sys.stderr.write('F  ')
577             sys.stderr.write(str(test))
578             sys.stderr.write('\n')
579         else:
580             sys.stderr.write('F')
581 
582 
583 class BSTestRunner(Template_mixin):
584     """
585     """
586     def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
587         self.stream = stream
588         self.verbosity = verbosity
589         if title is None:
590             self.title = self.DEFAULT_TITLE
591         else:
592             self.title = title
593         if description is None:
594             self.description = self.DEFAULT_DESCRIPTION
595         else:
596             self.description = description
597 
598         self.startTime = datetime.datetime.now()
599 
600 
601     def run(self, test):
602         "Run the given test case or test suite."
603         result = _TestResult(self.verbosity)
604         test(result)
605         self.stopTime = datetime.datetime.now()
606         self.generateReport(test, result)
607         # print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
608         sys.stderr.write('\nTime Elapsed: %s' % (self.stopTime-self.startTime))
609         return result
610 
611 
612     def sortResult(self, result_list):
613         # unittest does not seems to run in any particular order.
614         # Here at least we want to group them together by class.
615         rmap = {}
616         classes = []
617         for n,t,o,e in result_list:
618             cls = t.__class__
619             # if not rmap.has_key(cls):
620             if not cls in rmap:
621                 rmap[cls] = []
622                 classes.append(cls)
623             rmap[cls].append((n,t,o,e))
624         r = [(cls, rmap[cls]) for cls in classes]
625         return r
626 
627 
628     def getReportAttributes(self, result):
629         """
630         Return report attributes as a list of (name, value).
631         Override this to add custom attributes.
632         """
633         startTime = str(self.startTime)[:19]
634         duration = str(self.stopTime - self.startTime)
635         status = []
636         if result.success_count: status.append('<span class="text text-success">Pass <strong>%s</strong></span>'    % result.success_count)
637         if result.failure_count: status.append('<span class="text text-danger">Failure <strong>%s</strong></span>' % result.failure_count)
638         if result.error_count:   status.append('<span class="text text-warning">Error <strong>%s</strong></span>'   % result.error_count  )
639         if status:
640             status = ' '.join(status)
641         else:
642             status = 'none'
643         return [
644             ('Start Time', startTime),
645             ('Duration', duration),
646             ('Status', status),
647         ]
648 
649 
650     def generateReport(self, test, result):
651         report_attrs = self.getReportAttributes(result)
652         generator = 'BSTestRunner %s' % __version__
653         stylesheet = self._generate_stylesheet()
654         heading = self._generate_heading(report_attrs)
655         report = self._generate_report(result)
656         ending = self._generate_ending()
657         output = self.HTML_TMPL % dict(
658             title = saxutils.escape(self.title),
659             generator = generator,
660             stylesheet = stylesheet,
661             heading = heading,
662             report = report,
663             ending = ending,
664         )
665         try:
666             self.stream.write(output.encode('utf8'))
667         except:
668             self.stream.write(output)
669 
670 
671     def _generate_stylesheet(self):
672         return self.STYLESHEET_TMPL
673 
674 
675     def _generate_heading(self, report_attrs):
676         a_lines = []
677         for name, value in report_attrs:
678             line = self.HEADING_ATTRIBUTE_TMPL % dict(
679                     # name = saxutils.escape(name),
680                     # value = saxutils.escape(value),
681                     name = name,
682                     value = value,
683                 )
684             a_lines.append(line)
685         heading = self.HEADING_TMPL % dict(
686             title = saxutils.escape(self.title),
687             parameters = ''.join(a_lines),
688             description = saxutils.escape(self.description),
689         )
690         return heading
691 
692 
693     def _generate_report(self, result):
694         rows = []
695         sortedResult = self.sortResult(result.result)
696         for cid, (cls, cls_results) in enumerate(sortedResult):
697             # subtotal for a class
698             np = nf = ne = 0
699             for n,t,o,e in cls_results:
700                 if n == 0: np += 1
701                 elif n == 1: nf += 1
702                 else: ne += 1
703 
704             # format class description
705             if cls.__module__ == "__main__":
706                 name = cls.__name__
707             else:
708                 name = "%s.%s" % (cls.__module__, cls.__name__)
709             doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
710             desc = doc and '%s: %s' % (name, doc) or name
711 
712             row = self.REPORT_CLASS_TMPL % dict(
713                 style = ne > 0 and 'text text-warning' or nf > 0 and 'text text-danger' or 'text text-success',
714                 desc = desc,
715                 count = np+nf+ne,
716                 Pass = np,
717                 fail = nf,
718                 error = ne,
719                 cid = 'c%s' % (cid+1),
720             )
721             rows.append(row)
722 
723             for tid, (n,t,o,e) in enumerate(cls_results):
724                 self._generate_report_test(rows, cid, tid, n, t, o, e)
725 
726         report = self.REPORT_TMPL % dict(
727             test_list = ''.join(rows),
728             count = str(result.success_count+result.failure_count+result.error_count),
729             Pass = str(result.success_count),
730             fail = str(result.failure_count),
731             error = str(result.error_count),
732         )
733         return report
734 
735 
736     def _generate_report_test(self, rows, cid, tid, n, t, o, e):
737         # e.g. 'pt1.1', 'ft1.1', etc
738         has_output = bool(o or e)
739         tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
740         name = t.id().split('.')[-1]
741         doc = t.shortDescription() or ""
742         desc = doc and ('%s: %s' % (name, doc)) or name
743         tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
744 
745         # o and e should be byte string because they are collected from stdout and stderr?
746         if isinstance(o,str):
747             # TODO: some problem with 'string_escape': it escape \n and mess up formating
748             # uo = unicode(o.encode('string_escape'))
749             try:
750                 uo = o.decode('latin-1')
751             except:
752                 uo = o
753         else:
754             uo = o
755         if isinstance(e,str):
756             # TODO: some problem with 'string_escape': it escape \n and mess up formating
757             # ue = unicode(e.encode('string_escape'))
758             try:
759                 ue = e.decode('latin-1')
760             except:
761                 ue = e
762         else:
763             ue = e
764 
765         script = self.REPORT_TEST_OUTPUT_TMPL % dict(
766             id = tid,
767             output = saxutils.escape(uo+ue),
768         )
769 
770         row = tmpl % dict(
771             tid = tid,
772             # Class = (n == 0 and 'hiddenRow' or 'none'),
773             Class = (n == 0 and 'hiddenRow' or 'text text-success'),
774             # style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
775             style = n == 2 and 'text text-warning' or (n == 1 and 'text text-danger' or 'text text-success'),
776             desc = desc,
777             script = script,
778             status = self.STATUS[n],
779         )
780         rows.append(row)
781         if not has_output:
782             return
783 
784     def _generate_ending(self):
785         return self.ENDING_TMPL
786 
787 
788 ##############################################################################
789 # Facilities for running tests from the command line
790 ##############################################################################
791 
792 # Note: Reuse unittest.TestProgram to launch test. In the future we may
793 # build our own launcher to support more specific command line
794 # parameters like test title, CSS, etc.
795 class TestProgram(unittest.TestProgram):
796     """
797     A variation of the unittest.TestProgram. Please refer to the base
798     class for command line parameters.
799     """
800     def runTests(self):
801         # Pick BSTestRunner as the default test runner.
802         # base class's testRunner parameter is not useful because it means
803         # we have to instantiate BSTestRunner before we know self.verbosity.
804         if self.testRunner is None:
805             self.testRunner = BSTestRunner(verbosity=self.verbosity)
806         unittest.TestProgram.runTests(self)
807 
808 main = TestProgram
809 
810 ##############################################################################
811 # Executing this module from the command line
812 ##############################################################################
813 
814 if __name__ == "__main__":
815     main(module=None)
View Code

 

3. 将HTMLTestRunner.py 或者 BSTestRunner.py 放到 "

/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5

"(以mac为例)

 

4.主运行文件 

 1 # -*- coding:utf-8 -*-
 2 import unittest
 3 import os
 4 import time
 5 import HTMLTestRunner
 6 from BSTestRunner import BSTestRunner
 7 
 8 def allTests():
 9     path = os.path.dirname(__file__)
10     print(path)
11     suit = unittest.defaultTestLoader.discover(path,pattern='test2.py')
12     return suit
13 
14 def getNowTime():
15     return time.strftime('%Y-%m-%d %H_%M_%S')
16 
17 def run():
18     fileName = os.path.join(os.path.dirname(__file__),getNowTime()+'report.html')
19     fp = open(fileName,'wb')
20     runner = HTMLTestRunner.HTMLTestRunner(stream=fp,title='UI 自动化测试报告',description="详情")
21     # runner = BSTestRunner(stream=fp,title='自动化测试报告',description='详情')
22     runner.run(allTests())
23 
24 if __name__ == '__main__':
25     run()

 

 

5. HTMLTestRunner  report 

 

7. 代码覆盖率

pip3 install coverage

1 coverage3 run ttst.py #上面代码的主运行文件
2 coverage3 html

 

 会生成htmlcov 的文件夹,打开index.html

 

 

 

 8. BSTestRunner report

 

posted @ 2019-09-01 20:36  东方不败--Never  阅读(402)  评论(0编辑  收藏  举报