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,'&'); 281 s = s.replace(/</g,'<'); 282 s = s.replace(/>/g,'>'); 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> </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'> </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)
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,'&'); 296 s = s.replace(/</g,'<'); 297 s = s.replace(/>/g,'>'); 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> </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'> </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)
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