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. 将HTMLTestRunner.py 放到 "
/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5
"(以mac为例)
3.主运行文件
1 # -*- coding:utf-8 -*- 2 import unittest 3 import os 4 import time 5 import HTMLTestRunner 6 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.run(allTests()) 22 23 if __name__ == '__main__': 24 run()
4. report
5. 代码覆盖率
pip3 install coverage
1 coverage3 run ttst.py #上面代码的主运行文件 2 coverage3 html
会生成htmlcov 的文件夹,打开index.html