unittest 生成测试报告
前置:下载HTMLTestRunner.py文件,放在D:\Programs\Python\Python37\Lib\site-packages目录下
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.2" 69 70 71 """ 72 Change History 73 74 Version 0.8.2 75 * Show output inline instead of popup window (Viorel Lupu). 76 77 Version in 0.8.1 78 * Validated XHTML (Wolfgang Borgert). 79 * Added description of test classes and test cases. 80 81 Version in 0.8.0 82 * Define Template_mixin class for customization. 83 * Workaround a IE 6 bug that it does not treat <script> block as CDATA. 84 85 Version in 0.7.1 86 * Back port to Python 2.3 (Frank Horowitz). 87 * Fix missing scroll bars in detail log (Podi). 88 """ 89 90 # TODO: color stderr 91 # TODO: simplify javascript using ,ore than 1 class in the class attribute? 92 93 import datetime 94 #2.7版本为 import StringIO 95 import io 96 import sys 97 import time 98 import unittest 99 from xml.sax import saxutils 100 101 102 # ------------------------------------------------------------------------ 103 # The redirectors below are used to capture output during testing. Output 104 # sent to sys.stdout and sys.stderr are automatically captured. However 105 # in some cases sys.stdout is already cached before HTMLTestRunner is 106 # invoked (e.g. calling logging.basicConfig). In order to capture those 107 # output, use the redirectors for the cached stream. 108 # 109 # e.g. 110 # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) 111 # >>> 112 113 class OutputRedirector(object): 114 """ Wrapper to redirect stdout or stderr """ 115 def __init__(self, fp): 116 self.fp = fp 117 118 def write(self, s): 119 self.fp.write(s) 120 121 def writelines(self, lines): 122 self.fp.writelines(lines) 123 124 def flush(self): 125 self.fp.flush() 126 127 stdout_redirector = OutputRedirector(sys.stdout) 128 stderr_redirector = OutputRedirector(sys.stderr) 129 130 131 132 # ---------------------------------------------------------------------- 133 # Template 134 135 class Template_mixin(object): 136 """ 137 Define a HTML template for report customerization and generation. 138 139 Overall structure of an HTML report 140 141 HTML 142 +------------------------+ 143 |<html> | 144 | <head> | 145 | | 146 | STYLESHEET | 147 | +----------------+ | 148 | | | | 149 | +----------------+ | 150 | | 151 | </head> | 152 | | 153 | <body> | 154 | | 155 | HEADING | 156 | +----------------+ | 157 | | | | 158 | +----------------+ | 159 | | 160 | REPORT | 161 | +----------------+ | 162 | | | | 163 | +----------------+ | 164 | | 165 | ENDING | 166 | +----------------+ | 167 | | | | 168 | +----------------+ | 169 | | 170 | </body> | 171 |</html> | 172 +------------------------+ 173 """ 174 175 STATUS = { 176 0: 'pass', 177 1: 'fail', 178 2: 'error', 179 } 180 181 DEFAULT_TITLE = 'Unit Test Report' 182 DEFAULT_DESCRIPTION = '' 183 184 # ------------------------------------------------------------------------ 185 # HTML Template 186 187 HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> 188 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 189 <html xmlns="http://www.w3.org/1999/xhtml"> 190 <head> 191 <title>%(title)s</title> 192 <meta name="generator" content="%(generator)s"/> 193 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 194 %(stylesheet)s 195 </head> 196 <body> 197 <script language="javascript" type="text/javascript"><!-- 198 output_list = Array(); 199 200 /* level - 0:Summary; 1:Failed; 2:All */ 201 function showCase(level) { 202 trs = document.getElementsByTagName("tr"); 203 for (var i = 0; i < trs.length; i++) { 204 tr = trs[i]; 205 id = tr.id; 206 if (id.substr(0,2) == 'ft') { 207 if (level < 1) { 208 tr.className = 'hiddenRow'; 209 } 210 else { 211 tr.className = ''; 212 } 213 } 214 if (id.substr(0,2) == 'pt') { 215 if (level > 1) { 216 tr.className = ''; 217 } 218 else { 219 tr.className = 'hiddenRow'; 220 } 221 } 222 } 223 } 224 225 226 function showClassDetail(cid, count) { 227 var id_list = Array(count); 228 var toHide = 1; 229 for (var i = 0; i < count; i++) { 230 tid0 = 't' + cid.substr(1) + '.' + (i+1); 231 tid = 'f' + tid0; 232 tr = document.getElementById(tid); 233 if (!tr) { 234 tid = 'p' + tid0; 235 tr = document.getElementById(tid); 236 } 237 id_list[i] = tid; 238 if (tr.className) { 239 toHide = 0; 240 } 241 } 242 for (var i = 0; i < count; i++) { 243 tid = id_list[i]; 244 if (toHide) { 245 document.getElementById('div_'+tid).style.display = 'none' 246 document.getElementById(tid).className = 'hiddenRow'; 247 } 248 else { 249 document.getElementById(tid).className = ''; 250 } 251 } 252 } 253 254 255 function showTestDetail(div_id){ 256 var details_div = document.getElementById(div_id) 257 var displayState = details_div.style.display 258 // alert(displayState) 259 if (displayState != 'block' ) { 260 displayState = 'block' 261 details_div.style.display = 'block' 262 } 263 else { 264 details_div.style.display = 'none' 265 } 266 } 267 268 269 function html_escape(s) { 270 s = s.replace(/&/g,'&'); 271 s = s.replace(/</g,'<'); 272 s = s.replace(/>/g,'>'); 273 return s; 274 } 275 276 /* obsoleted by detail in <div> 277 function showOutput(id, name) { 278 var w = window.open("", //url 279 name, 280 "resizable,scrollbars,status,width=800,height=450"); 281 d = w.document; 282 d.write("<pre>"); 283 d.write(html_escape(output_list[id])); 284 d.write("\n"); 285 d.write("<a href='javascript:window.close()'>close</a>\n"); 286 d.write("</pre>\n"); 287 d.close(); 288 } 289 */ 290 --></script> 291 292 %(heading)s 293 %(report)s 294 %(ending)s 295 296 </body> 297 </html> 298 """ 299 # variables: (title, generator, stylesheet, heading, report, ending) 300 301 302 # ------------------------------------------------------------------------ 303 # Stylesheet 304 # 305 # alternatively use a <link> for external style sheet, e.g. 306 # <link rel="stylesheet" href="$url" type="text/css"> 307 308 STYLESHEET_TMPL = """ 309 <style type="text/css" media="screen"> 310 body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; } 311 table { font-size: 100%; } 312 pre { } 313 314 /* -- heading ---------------------------------------------------------------------- */ 315 h1 { 316 font-size: 16pt; 317 color: gray; 318 } 319 .heading { 320 margin-top: 0ex; 321 margin-bottom: 1ex; 322 } 323 324 .heading .attribute { 325 margin-top: 1ex; 326 margin-bottom: 0; 327 } 328 329 .heading .description { 330 margin-top: 4ex; 331 margin-bottom: 6ex; 332 } 333 334 /* -- css div popup ------------------------------------------------------------------------ */ 335 a.popup_link { 336 } 337 338 a.popup_link:hover { 339 color: red; 340 } 341 342 .popup_window { 343 display: none; 344 position: relative; 345 left: 0px; 346 top: 0px; 347 /*border: solid #627173 1px; */ 348 padding: 10px; 349 background-color: #E6E6D6; 350 font-family: "Lucida Console", "Courier New", Courier, monospace; 351 text-align: left; 352 font-size: 8pt; 353 width: 500px; 354 } 355 356 } 357 /* -- report ------------------------------------------------------------------------ */ 358 #show_detail_line { 359 margin-top: 3ex; 360 margin-bottom: 1ex; 361 } 362 #result_table { 363 width: 80%; 364 border-collapse: collapse; 365 border: 1px solid #777; 366 } 367 #header_row { 368 font-weight: bold; 369 color: white; 370 background-color: #777; 371 } 372 #result_table td { 373 border: 1px solid #777; 374 padding: 2px; 375 } 376 #total_row { font-weight: bold; } 377 .passClass { background-color: #6c6; } 378 .failClass { background-color: #c60; } 379 .errorClass { background-color: #c00; } 380 .passCase { color: #6c6; } 381 .failCase { color: #c60; font-weight: bold; } 382 .errorCase { color: #c00; font-weight: bold; } 383 .hiddenRow { display: none; } 384 .testcase { margin-left: 2em; } 385 386 387 /* -- ending ---------------------------------------------------------------------- */ 388 #ending { 389 } 390 391 </style> 392 """ 393 394 395 396 # ------------------------------------------------------------------------ 397 # Heading 398 # 399 400 HEADING_TMPL = """<div class='heading'> 401 <h1>%(title)s</h1> 402 %(parameters)s 403 <p class='description'>%(description)s</p> 404 </div> 405 406 """ # variables: (title, parameters, description) 407 408 HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p> 409 """ # variables: (name, value) 410 411 412 413 # ------------------------------------------------------------------------ 414 # Report 415 # 416 417 REPORT_TMPL = """ 418 <p id='show_detail_line'>Show 419 <a href='javascript:showCase(0)'>Summary</a> 420 <a href='javascript:showCase(1)'>Failed</a> 421 <a href='javascript:showCase(2)'>All</a> 422 </p> 423 <table id='result_table'> 424 <colgroup> 425 <col align='left' /> 426 <col align='right' /> 427 <col align='right' /> 428 <col align='right' /> 429 <col align='right' /> 430 <col align='right' /> 431 </colgroup> 432 <tr id='header_row'> 433 <td>Test Group/Test case</td> 434 <td>Count</td> 435 <td>Pass</td> 436 <td>Fail</td> 437 <td>Error</td> 438 <td>View</td> 439 </tr> 440 %(test_list)s 441 <tr id='total_row'> 442 <td>Total</td> 443 <td>%(count)s</td> 444 <td>%(Pass)s</td> 445 <td>%(fail)s</td> 446 <td>%(error)s</td> 447 <td> </td> 448 </tr> 449 </table> 450 """ # variables: (test_list, count, Pass, fail, error) 451 452 REPORT_CLASS_TMPL = r""" 453 <tr class='%(style)s'> 454 <td>%(desc)s</td> 455 <td>%(count)s</td> 456 <td>%(Pass)s</td> 457 <td>%(fail)s</td> 458 <td>%(error)s</td> 459 <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td> 460 </tr> 461 """ # variables: (style, desc, count, Pass, fail, error, cid) 462 463 464 REPORT_TEST_WITH_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'> 468 469 <!--css div popup start--> 470 <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > 471 %(status)s</a> 472 473 <div id='div_%(tid)s' class="popup_window"> 474 <div style='text-align: right; color:red;cursor:pointer'> 475 <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " > 476 [x]</a> 477 </div> 478 <pre> 479 %(script)s 480 </pre> 481 </div> 482 <!--css div popup end--> 483 484 </td> 485 </tr> 486 """ # variables: (tid, Class, style, desc, status) 487 488 489 REPORT_TEST_NO_OUTPUT_TMPL = r""" 490 <tr id='%(tid)s' class='%(Class)s'> 491 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 492 <td colspan='5' align='center'>%(status)s</td> 493 </tr> 494 """ # variables: (tid, Class, style, desc, status) 495 496 497 REPORT_TEST_OUTPUT_TMPL = r""" 498 %(id)s: %(output)s 499 """ # variables: (id, output) 500 501 502 503 # ------------------------------------------------------------------------ 504 # ENDING 505 # 506 507 ENDING_TMPL = """<div id='ending'> </div>""" 508 509 # -------------------- The end of the Template class ------------------- 510 511 512 TestResult = unittest.TestResult 513 514 class _TestResult(TestResult): 515 # note: _TestResult is a pure representation of results. 516 # It lacks the output and reporting ability compares to unittest._TextTestResult. 517 518 def __init__(self, verbosity=1): 519 TestResult.__init__(self) 520 self.stdout0 = None 521 self.stderr0 = None 522 self.success_count = 0 523 self.failure_count = 0 524 self.error_count = 0 525 self.verbosity = verbosity 526 527 # result is a list of result in 4 tuple 528 # ( 529 # result code (0: success; 1: fail; 2: error), 530 # TestCase object, 531 # Test output (byte string), 532 # stack trace, 533 # ) 534 self.result = [] 535 536 537 def startTest(self, test): 538 TestResult.startTest(self, test) 539 # just one buffer for both stdout and stderr 540 # 2.7版本为 self.outputBuffer = StringIO.StringIO() 541 self.outputBuffer = io.StringIO() 542 stdout_redirector.fp = self.outputBuffer 543 stderr_redirector.fp = self.outputBuffer 544 self.stdout0 = sys.stdout 545 self.stderr0 = sys.stderr 546 sys.stdout = stdout_redirector 547 sys.stderr = stderr_redirector 548 549 550 def complete_output(self): 551 """ 552 Disconnect output redirection and return buffer. 553 Safe to call multiple times. 554 """ 555 if self.stdout0: 556 sys.stdout = self.stdout0 557 sys.stderr = self.stderr0 558 self.stdout0 = None 559 self.stderr0 = None 560 return self.outputBuffer.getvalue() 561 562 563 def stopTest(self, test): 564 # Usually one of addSuccess, addError or addFailure would have been called. 565 # But there are some path in unittest that would bypass this. 566 # We must disconnect stdout in stopTest(), which is guaranteed to be called. 567 self.complete_output() 568 569 570 def addSuccess(self, test): 571 self.success_count += 1 572 TestResult.addSuccess(self, test) 573 output = self.complete_output() 574 self.result.append((0, test, output, '')) 575 if self.verbosity > 1: 576 sys.stderr.write('ok ') 577 sys.stderr.write(str(test)) 578 sys.stderr.write('\n') 579 else: 580 sys.stderr.write('.') 581 582 def addError(self, test, err): 583 self.error_count += 1 584 TestResult.addError(self, test, err) 585 _, _exc_str = self.errors[-1] 586 output = self.complete_output() 587 self.result.append((2, test, output, _exc_str)) 588 if self.verbosity > 1: 589 sys.stderr.write('E ') 590 sys.stderr.write(str(test)) 591 sys.stderr.write('\n') 592 else: 593 sys.stderr.write('E') 594 595 def addFailure(self, test, err): 596 self.failure_count += 1 597 TestResult.addFailure(self, test, err) 598 _, _exc_str = self.failures[-1] 599 output = self.complete_output() 600 self.result.append((1, test, output, _exc_str)) 601 if self.verbosity > 1: 602 sys.stderr.write('F ') 603 sys.stderr.write(str(test)) 604 sys.stderr.write('\n') 605 else: 606 sys.stderr.write('F') 607 608 609 class HTMLTestRunner(Template_mixin): 610 """ 611 """ 612 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 613 self.stream = stream 614 self.verbosity = verbosity 615 if title is None: 616 self.title = self.DEFAULT_TITLE 617 else: 618 self.title = title 619 if description is None: 620 self.description = self.DEFAULT_DESCRIPTION 621 else: 622 self.description = description 623 624 self.startTime = datetime.datetime.now() 625 626 627 def run(self, test): 628 "Run the given test case or test suite." 629 result = _TestResult(self.verbosity) 630 test(result) 631 self.stopTime = datetime.datetime.now() 632 self.generateReport(test, result) 633 print(sys.stderr,'\nTime Elapsed=%s' %(self.stopTime-self.startTime)) 634 #2.7版本 print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime) 635 return result 636 637 638 639 def sortResult(self, result_list): 640 # unittest does not seems to run in any particular order. 641 # Here at least we want to group them together by class. 642 rmap = {} 643 classes = [] 644 for n,t,o,e in result_list: 645 cls = t.__class__ 646 # 2.7版本 if not rmap.has_key(cls) 647 if not cls in rmap: 648 rmap[cls] = [] 649 classes.append(cls) 650 rmap[cls].append((n,t,o,e)) 651 r = [(cls, rmap[cls]) for cls in classes] 652 return r 653 654 655 def getReportAttributes(self, result): 656 """ 657 Return report attributes as a list of (name, value). 658 Override this to add custom attributes. 659 """ 660 startTime = str(self.startTime)[:19] 661 duration = str(self.stopTime - self.startTime) 662 status = [] 663 if result.success_count: status.append('Pass %s' % result.success_count) 664 if result.failure_count: status.append('Failure %s' % result.failure_count) 665 if result.error_count: status.append('Error %s' % result.error_count ) 666 if status: 667 status = ' '.join(status) 668 else: 669 status = 'none' 670 return [ 671 ('Start Time', startTime), 672 ('Duration', duration), 673 ('Status', status), 674 ] 675 676 677 def generateReport(self, test, result): 678 report_attrs = self.getReportAttributes(result) 679 generator = 'HTMLTestRunner %s' % __version__ 680 stylesheet = self._generate_stylesheet() 681 heading = self._generate_heading(report_attrs) 682 report = self._generate_report(result) 683 ending = self._generate_ending() 684 output = self.HTML_TMPL % dict( 685 title = saxutils.escape(self.title), 686 generator = generator, 687 stylesheet = stylesheet, 688 heading = heading, 689 report = report, 690 ending = ending, 691 ) 692 self.stream.write(output.encode('utf8')) 693 694 695 def _generate_stylesheet(self): 696 return self.STYLESHEET_TMPL 697 698 699 def _generate_heading(self, report_attrs): 700 a_lines = [] 701 for name, value in report_attrs: 702 line = self.HEADING_ATTRIBUTE_TMPL % dict( 703 name = saxutils.escape(name), 704 value = saxutils.escape(value), 705 ) 706 a_lines.append(line) 707 heading = self.HEADING_TMPL % dict( 708 title = saxutils.escape(self.title), 709 parameters = ''.join(a_lines), 710 description = saxutils.escape(self.description), 711 ) 712 return heading 713 714 715 def _generate_report(self, result): 716 rows = [] 717 sortedResult = self.sortResult(result.result) 718 for cid, (cls, cls_results) in enumerate(sortedResult): 719 # subtotal for a class 720 np = nf = ne = 0 721 for n,t,o,e in cls_results: 722 if n == 0: np += 1 723 elif n == 1: nf += 1 724 else: ne += 1 725 726 # format class description 727 if cls.__module__ == "__main__": 728 name = cls.__name__ 729 else: 730 name = "%s.%s" % (cls.__module__, cls.__name__) 731 doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 732 desc = doc and '%s: %s' % (name, doc) or name 733 734 row = self.REPORT_CLASS_TMPL % dict( 735 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 736 desc = desc, 737 count = np+nf+ne, 738 Pass = np, 739 fail = nf, 740 error = ne, 741 cid = 'c%s' % (cid+1), 742 ) 743 rows.append(row) 744 745 for tid, (n,t,o,e) in enumerate(cls_results): 746 self._generate_report_test(rows, cid, tid, n, t, o, e) 747 748 report = self.REPORT_TMPL % dict( 749 test_list = ''.join(rows), 750 count = str(result.success_count+result.failure_count+result.error_count), 751 Pass = str(result.success_count), 752 fail = str(result.failure_count), 753 error = str(result.error_count), 754 ) 755 return report 756 757 758 def _generate_report_test(self, rows, cid, tid, n, t, o, e): 759 # e.g. 'pt1.1', 'ft1.1', etc 760 has_output = bool(o or e) 761 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 762 name = t.id().split('.')[-1] 763 doc = t.shortDescription() or "" 764 desc = doc and ('%s: %s' % (name, doc)) or name 765 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 766 767 # o and e should be byte string because they are collected from stdout and stderr? 768 if isinstance(o,str): 769 uo = e 770 # TODO: some problem with 'string_escape': it escape \n and mess up formating 771 # uo = unicode(o.encode('string_escape')) 772 # 2.7版本uo = o.decode('latin-1') 773 else: 774 uo = o 775 if isinstance(e,str): 776 ue = e 777 # TODO: some problem with 'string_escape': it escape \n and mess up formating 778 # ue = unicode(e.encode('string_escape')) 779 # 2.7 版本 ue = e.decode('latin-1') 780 781 else: 782 ue = e 783 784 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 785 id = tid, 786 output = saxutils.escape(uo+ue), 787 ) 788 789 row = tmpl % dict( 790 tid = tid, 791 Class = (n == 0 and 'hiddenRow' or 'none'), 792 style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), 793 desc = desc, 794 script = script, 795 status = self.STATUS[n], 796 ) 797 rows.append(row) 798 if not has_output: 799 return 800 801 def _generate_ending(self): 802 return self.ENDING_TMPL 803 804 805 ############################################################################## 806 # Facilities for running tests from the command line 807 ############################################################################## 808 809 # Note: Reuse unittest.TestProgram to launch test. In the future we may 810 # build our own launcher to support more specific command line 811 # parameters like test title, CSS, etc. 812 class TestProgram(unittest.TestProgram): 813 """ 814 A variation of the unittest.TestProgram. Please refer to the base 815 class for command line parameters. 816 """ 817 def runTests(self): 818 # Pick HTMLTestRunner as the default test runner. 819 # base class's testRunner parameter is not useful because it means 820 # we have to instantiate HTMLTestRunner before we know self.verbosity. 821 if self.testRunner is None: 822 self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 823 unittest.TestProgram.runTests(self) 824 825 main = TestProgram 826 827 ############################################################################## 828 # Executing this module from the command line 829 ############################################################################## 830 831 if __name__ == "__main__": 832 main(module=None)
生成测试报告:
1 import unittest 2 import time 3 from selenium import webdriver 4 import HTMLTestRunner 5 6 7 class UILoginTest(unittest.TestCase): 8 9 def setUp(self) -> None: 10 self.driver = webdriver.Chrome() 11 self.imgs = [] # 初始化存放截图的列表 12 self.url = 'http://xxx/login' 13 14 def tearDown(self) -> None: 15 self.driver.quit() 16 17 def test_login_success(self): 18 self.driver.get(self.url) 19 self.driver.maximize_window() 20 self.driver.find_element_by_xpath('//*[@type="button"]/span[text()="登录"]').click() 21 time.sleep(0.5) 22 self.driver.find_element('xpath', '//*[@placeholder="用户名"]').send_keys('xxx') 23 self.driver.find_element('xpath', '//*[@placeholder="密码"]').send_keys('xxxxxx') 24 self.driver.find_element('xpath','//*[@type="button"]/span[text()="登\xa0\xa0录"]').click() 25 time.sleep(2) 26 # 执行截图操作,将当前截图加入测试报告中 27 self.imgs.append(self.driver.get_screenshot_as_base64()) 28 loginname = self.driver.find_element('xpath', '//*[@title="XX"]').text 29 # print(loginname) 30 self.assertEqual('XX', loginname) 31 32 def test_login_failed_without_username(self): 33 self.driver.get(self.url) 34 self.driver.maximize_window() 35 self.driver.find_element_by_xpath('//*[@type="button"]/span[text()="登录"]').click() 36 time.sleep(0.5) 37 self.driver.find_element('xpath', '//*[@placeholder="密码"]').send_keys('xxxxxx') 38 self.driver.find_element('xpath', '//*[@type="button"]/span[text()="登\xa0\xa0录"]').click() 39 time.sleep(0.5) 40 self.imgs.append(self.driver.get_screenshot_as_base64()) 41 errmsg = self.driver.find_element('xpath', '//div[contains(text(),"请输入用户名")]').text 42 # print(errmsg) 43 self.assertEqual(errmsg, '请输入用户名') 44 45 def test_login_failed_with_incorrect_username(self): 46 self.driver.get(self.url) 47 self.driver.maximize_window() 48 self.driver.find_element_by_xpath('//*[@type="button"]/span[text()="登录"]').click() 49 time.sleep(0.5) 50 self.driver.find_element('xpath', '//*[@placeholder="用户名"]').send_keys('xxx1') 51 self.driver.find_element('xpath', '//*[@placeholder="密码"]').send_keys('xxxxxx') 52 self.driver.find_element('xpath', '//*[@type="button"]/span[text()="登\xa0\xa0录"]').click() 53 time.sleep(0.5) 54 self.imgs.append(self.driver.get_screenshot_as_base64()) 55 errmsg = self.driver.find_element('xpath', '//p[contains(text(),"验证失败!")]').text 56 # print(errmsg) 57 self.assertEqual(errmsg, '验证失败!') 58 59 def test_login_failed_without_password(self): 60 self.driver.get(self.url) 61 self.driver.maximize_window() 62 self.driver.find_element_by_xpath('//*[@type="button"]/span[text()="登录"]').click() 63 time.sleep(0.5) 64 self.driver.find_element('xpath', '//*[@placeholder="用户名"]').send_keys('xxx') 65 self.driver.find_element('xpath', '//*[@type="button"]/span[text()="登\xa0\xa0录"]').click() 66 time.sleep(0.5) 67 self.imgs.append(self.driver.get_screenshot_as_base64()) 68 errmsg = self.driver.find_element('xpath', '//div[contains(text(),"请输入密码")]').text 69 # print(errmsg) 70 self.assertEqual(errmsg, '请输入密码') 71 72 73 if __name__ == '__main__': 74 test1 = unittest.defaultTestLoader.loadTestsFromTestCase(UILoginTest) 75 suite = unittest.TestSuite(test1) 76 77 runner = HTMLTestRunner.HTMLTestRunner( 78 title='xx系统自动化测试报告', 79 description='xxx测试报告', 80 stream=open('sample_test_report.html', 'wb'), 81 verbosity=2 82 ) 83 84 runner.run(suite)
本文来自博客园,作者:wujin啊~,转载请注明原文链接:https://www.cnblogs.com/wujina/p/16118039.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix