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 = """
401

%(title)s

402 %(parameters)s 403

%(description)s

404
405 406 """ # variables: (title, parameters, description) 407 408 HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

409 """ # variables: (name, value) 410 411 412 413 # ------------------------------------------------------------------------ 414 # Report 415 # 416 417 REPORT_TMPL = """ 418

Show 419 Summary 420 Failed 421 All 422

423 424425426427428429430431 432433 434 435 436 437 438 439440%(test_list)s 441442 443 444 445 446 447 448449
Test Group/Test case Count Pass Fail Error View
Total %(count)s %(Pass)s %(fail)s %(error)s  
450 """ # variables: (test_list, count, Pass, fail, error) 451 452 REPORT_CLASS_TMPL = r""" 453 454 %(desc)s 455 %(count)s 456 %(Pass)s 457 %(fail)s 458 %(error)s 459 Detail 460 461 """ # variables: (style, desc, count, Pass, fail, error, cid) 462 463 464 REPORT_TEST_WITH_OUTPUT_TMPL = r""" 465 466
%(desc)s
467 468 469 470 471 %(status)s 472 473 482 483 484 485 486 """ # variables: (tid, Class, style, desc, status) 487 488 489 REPORT_TEST_NO_OUTPUT_TMPL = r""" 490 491
%(desc)s
492 %(status)s 493 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 = """
 
""" 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)