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 = '' 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 | | 144 | | 145 | | 146 | STYLESHEET | 147 | +----------------+ | 148 | | | | 149 | +----------------+ | 150 | | 151 | | 152 | | 153 | | 154 | | 155 | HEADING | 156 | +----------------+ | 157 | | | | 158 | +----------------+ | 159 | | 160 | REPORT | 161 | +----------------+ | 162 | | | | 163 | +----------------+ | 164 | | 165 | ENDING | 166 | +----------------+ | 167 | | | | 168 | +----------------+ | 169 | | 170 | | 171 | | 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 189 190 191%(title)s 192 193 194 %(stylesheet)s 195 196 197 <script language="javascript" type="text/javascript"></script> 291 292 %(heading)s 293 %(report)s 294 %(ending)s 295 296 297 298 """ 299 # variables: (title, generator, stylesheet, heading, report, ending) 300 301 302 # ------------------------------------------------------------------------ 303 # Stylesheet 304 # 305 # alternatively use a for external style sheet, e.g. 306 # 307 308 STYLESHEET_TMPL = """ 309 392 """ 393 394 395 396 # ------------------------------------------------------------------------ 397 # Heading 398 # 399 400 HEADING_TMPL = """401405 406 """ # variables: (title, parameters, description) 407 408 HEADING_ATTRIBUTE_TMPL = """%(title)s
402 %(parameters)s 403%(description)s
404%(name)s: %(value)s
409 """ # variables: (name, value) 410 411 412 413 # ------------------------------------------------------------------------ 414 # Report 415 # 416 417 REPORT_TMPL = """ 418Show
419 Summary 420 Failed 421 All 422 423424
450 """ # variables: (test_list, count, Pass, fail, error) 451 452 REPORT_CLASS_TMPL = r""" 453425 426 427 428 429 430 431 432 433 440 %(test_list)s 441Test Group/Test case 434Count 435Pass 436Fail 437Error 438View 439442 449Total 443%(count)s 444%(Pass)s 445%(fail)s 446%(error)s 447448 454 461 """ # variables: (style, desc, count, Pass, fail, error, cid) 462 463 464 REPORT_TEST_WITH_OUTPUT_TMPL = r""" 465%(desc)s 455%(count)s 456%(Pass)s 457%(fail)s 458%(error)s 459Detail 460466 486 """ # variables: (tid, Class, style, desc, status) 487 488 489 REPORT_TEST_NO_OUTPUT_TMPL = r""" 490467 %(desc)s468 469 470 471 %(status)s 472 473 482 483 484 485491 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 = """492 %(desc)s%(status)s 493""" 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)