├── .gitignore ├── ExampleReport.html ├── HTMLTestRunner_PY3.py ├── README.md ├── __init__.py ├── img └── echarts.png └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/encodings.xml 3 | .idea/HTMLTestRunner_PY3.iml 4 | .idea/modules.xml 5 | .idea/modules.xml 6 | .idea/misc.xml 7 | .idea/misc.xml 8 | .idea/workspace.xml 9 | .idea/vcs.xml 10 | -------------------------------------------------------------------------------- /ExampleReport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example用例执行报告 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 100 | 101 | 102 | 103 | 104 | 198 | 199 |
200 | 201 | 208 |

用于展示修改样式后的HTMLTestRunner

209 |
210 | 211 | 212 |
213 | 214 | 215 | 216 |
217 |

218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 264 | 265 | 266 | 267 | 268 | 289 | 290 | 291 | 292 | 293 | 314 | 315 | 316 | 317 | 318 | 335 | 336 | 337 | 338 | 339 | 356 | 357 | 358 | 359 | 360 | 373 | 374 | 375 | 376 | 377 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 |
测试套件/测试用例总数通过失败错误查看
TestTest: 测试HTMLTestRunner 7331详情
test_a_divide_c: a / c = 1 这是个有subTest的用例
248 | 249 | 250 | 251 | 通过 252 | 253 | 261 | 262 | 263 |
test_a_divide_c: a / c = 1 这是个有subTest的用例
269 | 270 | 271 | 272 | 失败 273 | 274 | 286 | 287 | 288 |
test_a_divide_c: a / c = 1 这是个有subTest的用例
294 | 295 | 296 | 297 | 失败 298 | 299 | 311 | 312 | 313 |
test_a_error_case: 除零异常
319 | 320 | 321 | 322 | 错误 323 | 324 | 332 | 333 | 334 |
test_a_minus_b: a - b = 3 这个用例应该失败
340 | 341 | 342 | 343 | 失败 344 | 345 | 353 | 354 | 355 |
test_a_multi_b: a * b = 2 这个用例应该成功
361 | 362 | 363 | 364 | 通过 365 | 366 | 370 | 371 | 372 |
test_a_plus_b: a + b = 3 这个用例应该通过
378 | 379 | 380 | 381 | 通过 382 | 383 | 387 | 388 | 389 |
ExampleCase1: 此class包含两个用例:add - ok, minus - FAIL2110详情
test_add: 用例1,add,此用例成功通过
通过
test_minus: 用例2,minus,此用例执行失败,4-3!=2
409 | 410 | 411 | 412 | 失败 413 | 414 | 422 | 423 | 424 |
ExampleCase2: 此class包含一个用例:plus - ERROR1001详情
test_plus: 用例3,plus,此用例执行出错,因为c未定义
439 | 440 | 441 | 442 | 错误 443 | 444 | 451 | 452 | 453 |
ExampleCase3: 此class包含一个用例:divide - ok1100详情
test_devide: 用例4,divide,此用例执行成功
468 | 469 | 470 | 471 | 通过 472 | 473 | 478 | 479 | 480 |
总计11542 
492 | 493 |
 
494 | 495 | 540 | 541 |
542 | 543 | 544 | -------------------------------------------------------------------------------- /HTMLTestRunner_PY3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | A TestRunner for use with the Python unit testing framework. It 4 | generates a HTML report to show the result at a glance. 5 | 6 | The simplest way to use this is to invoke its main method. E.g. 7 | 8 | import unittest 9 | import HTMLTestRunner 10 | 11 | ... define your tests ... 12 | 13 | if __name__ == '__main__': 14 | HTMLTestRunner.main() 15 | 16 | 17 | For more customization options, instantiates a HTMLTestRunner object. 18 | HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. 19 | 20 | # output to a file 21 | fp = file('my_report.html', 'wb') 22 | runner = HTMLTestRunner.HTMLTestRunner( 23 | stream=fp, 24 | title='My unit test', 25 | description='This demonstrates the report output by HTMLTestRunner.' 26 | ) 27 | 28 | # Use an external stylesheet. 29 | # See the Template_mixin class for more customizable options 30 | runner.STYLESHEET_TMPL = '' 31 | 32 | # run the test 33 | runner.run(my_test_suite) 34 | 35 | 36 | ------------------------------------------------------------------------ 37 | Copyright (c) 2004-2007, Wai Yip Tung 38 | All rights reserved. 39 | 40 | Redistribution and use in source and binary forms, with or without 41 | modification, are permitted provided that the following conditions are 42 | met: 43 | 44 | * Redistributions of source code must retain the above copyright notice, 45 | this list of conditions and the following disclaimer. 46 | * Redistributions in binary form must reproduce the above copyright 47 | notice, this list of conditions and the following disclaimer in the 48 | documentation and/or other materials provided with the distribution. 49 | * Neither the name Wai Yip Tung nor the names of its contributors may be 50 | used to endorse or promote products derived from this software without 51 | specific prior written permission. 52 | 53 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 54 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 55 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 56 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 57 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 58 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 59 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 60 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 61 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 62 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 63 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 64 | """ 65 | 66 | # URL: http://tungwaiyip.info/software/HTMLTestRunner.html 67 | 68 | __author__ = "Wai Yip Tung" 69 | __version__ = "0.9.1" 70 | 71 | """ 72 | Change History 73 | Version 0.9.1 74 | * 用Echarts添加执行情况统计图 (灰蓝) 75 | 76 | Version 0.9.0 77 | * 改成Python 3.x (灰蓝) 78 | 79 | Version 0.8.3 80 | * 使用 Bootstrap稍加美化 (灰蓝) 81 | * 改为中文 (灰蓝) 82 | 83 | Version 0.8.2 84 | * Show output inline instead of popup window (Viorel Lupu). 85 | 86 | Version in 0.8.1 87 | * Validated XHTML (Wolfgang Borgert). 88 | * Added description of test classes and test cases. 89 | 90 | Version in 0.8.0 91 | * Define Template_mixin class for customization. 92 | * Workaround a IE 6 bug that it does not treat 205 | 206 | 207 | %(stylesheet)s 208 | 209 | 210 | 211 | 305 | 306 |
307 | %(heading)s 308 | %(report)s 309 | %(ending)s 310 | %(chart_script)s 311 |
312 | 313 | 314 | """ # variables: (title, generator, stylesheet, heading, report, ending, chart_script) 315 | 316 | ECHARTS_SCRIPT = """ 317 | 362 | """ # variables: (Pass, fail, error) 363 | 364 | # ------------------------------------------------------------------------ 365 | # Stylesheet 366 | # 367 | # alternatively use a for external style sheet, e.g. 368 | # 369 | 370 | STYLESHEET_TMPL = """ 371 | 457 | """ 458 | 459 | # ------------------------------------------------------------------------ 460 | # Heading 461 | # 462 | 463 | HEADING_TMPL = """ 464 | 468 |

%(description)s

469 |
470 | """ # variables: (title, parameters, description) 471 | 472 | HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

473 | """ # variables: (name, value) 474 | 475 | # ------------------------------------------------------------------------ 476 | # Report 477 | # 478 | 479 | REPORT_TMPL = u""" 480 |
481 | 482 | 483 | 484 |
485 |

486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | %(test_list)s 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 |
测试套件/测试用例总数通过失败错误查看
总计%(count)s%(Pass)s%(fail)s%(error)s 
513 | """ # variables: (test_list, count, Pass, fail, error) 514 | 515 | REPORT_CLASS_TMPL = u""" 516 | 517 | %(desc)s 518 | %(count)s 519 | %(Pass)s 520 | %(fail)s 521 | %(error)s 522 | 详情 523 | 524 | """ # variables: (style, desc, count, Pass, fail, error, cid) 525 | 526 | REPORT_TEST_WITH_OUTPUT_TMPL = r""" 527 | 528 |
%(desc)s
529 | 530 | 531 | 532 | 533 | %(status)s 534 | 535 | 538 | 539 | 540 | 541 | 542 | """ # variables: (tid, Class, style, desc, status) 543 | 544 | REPORT_TEST_NO_OUTPUT_TMPL = r""" 545 | 546 |
%(desc)s
547 | %(status)s 548 | 549 | """ # variables: (tid, Class, style, desc, status) 550 | 551 | REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s""" # variables: (id, output) 552 | 553 | # ------------------------------------------------------------------------ 554 | # ENDING 555 | # 556 | 557 | ENDING_TMPL = """
 
""" 558 | 559 | # -------------------- The end of the Template class ------------------- 560 | 561 | 562 | TestResult = unittest.TestResult 563 | 564 | 565 | class _TestResult(TestResult): 566 | # note: _TestResult is a pure representation of results. 567 | # It lacks the output and reporting ability compares to unittest._TextTestResult. 568 | 569 | def __init__(self, verbosity=1): 570 | TestResult.__init__(self) 571 | self.stdout0 = None 572 | self.stderr0 = None 573 | self.success_count = 0 574 | self.failure_count = 0 575 | self.error_count = 0 576 | self.verbosity = verbosity 577 | 578 | # result is a list of result in 4 tuple 579 | # ( 580 | # result code (0: success; 1: fail; 2: error), 581 | # TestCase object, 582 | # Test output (byte string), 583 | # stack trace, 584 | # ) 585 | self.result = [] 586 | self.subtestlist = [] 587 | 588 | def startTest(self, test): 589 | TestResult.startTest(self, test) 590 | # just one buffer for both stdout and stderr 591 | self.outputBuffer = io.StringIO() 592 | stdout_redirector.fp = self.outputBuffer 593 | stderr_redirector.fp = self.outputBuffer 594 | self.stdout0 = sys.stdout 595 | self.stderr0 = sys.stderr 596 | sys.stdout = stdout_redirector 597 | sys.stderr = stderr_redirector 598 | 599 | def complete_output(self): 600 | """ 601 | Disconnect output redirection and return buffer. 602 | Safe to call multiple times. 603 | """ 604 | if self.stdout0: 605 | sys.stdout = self.stdout0 606 | sys.stderr = self.stderr0 607 | self.stdout0 = None 608 | self.stderr0 = None 609 | return self.outputBuffer.getvalue() 610 | 611 | def stopTest(self, test): 612 | # Usually one of addSuccess, addError or addFailure would have been called. 613 | # But there are some path in unittest that would bypass this. 614 | # We must disconnect stdout in stopTest(), which is guaranteed to be called. 615 | self.complete_output() 616 | 617 | def addSuccess(self, test): 618 | if test not in self.subtestlist: 619 | self.success_count += 1 620 | TestResult.addSuccess(self, test) 621 | output = self.complete_output() 622 | self.result.append((0, test, output, '')) 623 | if self.verbosity > 1: 624 | sys.stderr.write('ok ') 625 | sys.stderr.write(str(test)) 626 | sys.stderr.write('\n') 627 | else: 628 | sys.stderr.write('.') 629 | 630 | def addError(self, test, err): 631 | self.error_count += 1 632 | TestResult.addError(self, test, err) 633 | _, _exc_str = self.errors[-1] 634 | output = self.complete_output() 635 | self.result.append((2, test, output, _exc_str)) 636 | if self.verbosity > 1: 637 | sys.stderr.write('E ') 638 | sys.stderr.write(str(test)) 639 | sys.stderr.write('\n') 640 | else: 641 | sys.stderr.write('E') 642 | 643 | def addFailure(self, test, err): 644 | self.failure_count += 1 645 | TestResult.addFailure(self, test, err) 646 | _, _exc_str = self.failures[-1] 647 | output = self.complete_output() 648 | self.result.append((1, test, output, _exc_str)) 649 | if self.verbosity > 1: 650 | sys.stderr.write('F ') 651 | sys.stderr.write(str(test)) 652 | sys.stderr.write('\n') 653 | else: 654 | sys.stderr.write('F') 655 | 656 | def addSubTest(self, test, subtest, err): 657 | if err is not None: 658 | if getattr(self, 'failfast', False): 659 | self.stop() 660 | if issubclass(err[0], test.failureException): 661 | self.failure_count += 1 662 | errors = self.failures 663 | errors.append((subtest, self._exc_info_to_string(err, subtest))) 664 | output = self.complete_output() 665 | self.result.append((1, test, output + '\nSubTestCase Failed:\n' + str(subtest), 666 | self._exc_info_to_string(err, subtest))) 667 | if self.verbosity > 1: 668 | sys.stderr.write('F ') 669 | sys.stderr.write(str(subtest)) 670 | sys.stderr.write('\n') 671 | else: 672 | sys.stderr.write('F') 673 | else: 674 | self.error_count += 1 675 | errors = self.errors 676 | errors.append((subtest, self._exc_info_to_string(err, subtest))) 677 | output = self.complete_output() 678 | self.result.append( 679 | (2, test, output + '\nSubTestCase Error:\n' + str(subtest), self._exc_info_to_string(err, subtest))) 680 | if self.verbosity > 1: 681 | sys.stderr.write('E ') 682 | sys.stderr.write(str(subtest)) 683 | sys.stderr.write('\n') 684 | else: 685 | sys.stderr.write('E') 686 | self._mirrorOutput = True 687 | else: 688 | self.subtestlist.append(subtest) 689 | self.subtestlist.append(test) 690 | self.success_count += 1 691 | output = self.complete_output() 692 | self.result.append((0, test, output + '\nSubTestCase Pass:\n' + str(subtest), '')) 693 | if self.verbosity > 1: 694 | sys.stderr.write('ok ') 695 | sys.stderr.write(str(subtest)) 696 | sys.stderr.write('\n') 697 | else: 698 | sys.stderr.write('.') 699 | 700 | 701 | class HTMLTestRunner(Template_mixin): 702 | 703 | def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 704 | self.stream = stream 705 | self.verbosity = verbosity 706 | if title is None: 707 | self.title = self.DEFAULT_TITLE 708 | else: 709 | self.title = title 710 | if description is None: 711 | self.description = self.DEFAULT_DESCRIPTION 712 | else: 713 | self.description = description 714 | 715 | self.startTime = datetime.datetime.now() 716 | 717 | def run(self, test): 718 | "Run the given test case or test suite." 719 | result = _TestResult(self.verbosity) 720 | test(result) 721 | self.stopTime = datetime.datetime.now() 722 | self.generateReport(test, result) 723 | print('\nTime Elapsed: %s' % (self.stopTime-self.startTime), file=sys.stderr) 724 | return result 725 | 726 | def sortResult(self, result_list): 727 | # unittest does not seems to run in any particular order. 728 | # Here at least we want to group them together by class. 729 | rmap = {} 730 | classes = [] 731 | for n,t,o,e in result_list: 732 | cls = t.__class__ 733 | if cls not in rmap: 734 | rmap[cls] = [] 735 | classes.append(cls) 736 | rmap[cls].append((n,t,o,e)) 737 | r = [(cls, rmap[cls]) for cls in classes] 738 | return r 739 | 740 | def getReportAttributes(self, result): 741 | """ 742 | Return report attributes as a list of (name, value). 743 | Override this to add custom attributes. 744 | """ 745 | startTime = str(self.startTime)[:19] 746 | duration = str(self.stopTime - self.startTime) 747 | status = [] 748 | if result.success_count: status.append(u'通过 %s' % result.success_count) 749 | if result.failure_count: status.append(u'失败 %s' % result.failure_count) 750 | if result.error_count: status.append(u'错误 %s' % result.error_count ) 751 | if status: 752 | status = ' '.join(status) 753 | else: 754 | status = 'none' 755 | return [ 756 | (u'开始时间', startTime), 757 | (u'运行时长', duration), 758 | (u'状态', status), 759 | ] 760 | 761 | def generateReport(self, test, result): 762 | report_attrs = self.getReportAttributes(result) 763 | generator = 'HTMLTestRunner %s' % __version__ 764 | stylesheet = self._generate_stylesheet() 765 | heading = self._generate_heading(report_attrs) 766 | report = self._generate_report(result) 767 | ending = self._generate_ending() 768 | chart = self._generate_chart(result) 769 | output = self.HTML_TMPL % dict( 770 | title = saxutils.escape(self.title), 771 | generator = generator, 772 | stylesheet = stylesheet, 773 | heading = heading, 774 | report = report, 775 | ending = ending, 776 | chart_script = chart 777 | ) 778 | self.stream.write(output.encode('utf8')) 779 | 780 | def _generate_stylesheet(self): 781 | return self.STYLESHEET_TMPL 782 | 783 | def _generate_heading(self, report_attrs): 784 | a_lines = [] 785 | for name, value in report_attrs: 786 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 787 | name = saxutils.escape(name), 788 | value = saxutils.escape(value), 789 | ) 790 | a_lines.append(line) 791 | heading = self.HEADING_TMPL % dict( 792 | title = saxutils.escape(self.title), 793 | parameters = ''.join(a_lines), 794 | description = saxutils.escape(self.description), 795 | ) 796 | return heading 797 | 798 | def _generate_report(self, result): 799 | rows = [] 800 | sortedResult = self.sortResult(result.result) 801 | for cid, (cls, cls_results) in enumerate(sortedResult): 802 | # subtotal for a class 803 | np = nf = ne = 0 804 | for n,t,o,e in cls_results: 805 | if n == 0: np += 1 806 | elif n == 1: nf += 1 807 | else: ne += 1 808 | 809 | # format class description 810 | if cls.__module__ == "__main__": 811 | name = cls.__name__ 812 | else: 813 | name = "%s.%s" % (cls.__module__, cls.__name__) 814 | doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 815 | desc = doc and '%s: %s' % (name, doc) or name 816 | 817 | row = self.REPORT_CLASS_TMPL % dict( 818 | style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 819 | desc = desc, 820 | count = np+nf+ne, 821 | Pass = np, 822 | fail = nf, 823 | error = ne, 824 | cid = 'c%s' % (cid+1), 825 | ) 826 | rows.append(row) 827 | 828 | for tid, (n,t,o,e) in enumerate(cls_results): 829 | self._generate_report_test(rows, cid, tid, n, t, o, e) 830 | 831 | report = self.REPORT_TMPL % dict( 832 | test_list = ''.join(rows), 833 | count = str(result.success_count+result.failure_count+result.error_count), 834 | Pass = str(result.success_count), 835 | fail = str(result.failure_count), 836 | error = str(result.error_count), 837 | ) 838 | return report 839 | 840 | def _generate_chart(self, result): 841 | chart = self.ECHARTS_SCRIPT % dict( 842 | Pass=str(result.success_count), 843 | fail=str(result.failure_count), 844 | error=str(result.error_count), 845 | ) 846 | return chart 847 | 848 | def _generate_report_test(self, rows, cid, tid, n, t, o, e): 849 | # e.g. 'pt1.1', 'ft1.1', etc 850 | has_output = bool(o or e) 851 | tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 852 | name = t.id().split('.')[-1] 853 | doc = t.shortDescription() or "" 854 | desc = doc and ('%s: %s' % (name, doc)) or name 855 | tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 856 | 857 | script = self.REPORT_TEST_OUTPUT_TMPL % dict( 858 | id=tid, 859 | output=saxutils.escape(o+e), 860 | ) 861 | 862 | row = tmpl % dict( 863 | tid=tid, 864 | Class=(n == 0 and 'hiddenRow' or 'none'), 865 | style=(n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none')), 866 | desc=desc, 867 | script=script, 868 | status=self.STATUS[n], 869 | ) 870 | rows.append(row) 871 | if not has_output: 872 | return 873 | 874 | def _generate_ending(self): 875 | return self.ENDING_TMPL 876 | 877 | 878 | ############################################################################## 879 | # Facilities for running tests from the command line 880 | ############################################################################## 881 | 882 | # Note: Reuse unittest.TestProgram to launch test. In the future we may 883 | # build our own launcher to support more specific command line 884 | # parameters like test title, CSS, etc. 885 | class TestProgram(unittest.TestProgram): 886 | """ 887 | A variation of the unittest.TestProgram. Please refer to the base 888 | class for command line parameters. 889 | """ 890 | def runTests(self): 891 | # Pick HTMLTestRunner as the default test runner. 892 | # base class's testRunner parameter is not useful because it means 893 | # we have to instantiate HTMLTestRunner before we know self.verbosity. 894 | if self.testRunner is None: 895 | self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 896 | unittest.TestProgram.runTests(self) 897 | 898 | main = TestProgram 899 | 900 | ############################################################################## 901 | # Executing this module from the command line 902 | ############################################################################## 903 | 904 | if __name__ == "__main__": 905 | main(module=None) 906 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTMLTestRunner_PY3 2 | 3 | ## 2017.09.08 统计图 0.9.1 4 | 5 | 用Echarts添加了一个统计图,如下图: 6 | 7 | ![测试执行情况统计图](./img/echarts.png) 8 | 9 | **(2017.12.22) 修改统计饼图颜色** 10 | 11 | **(2017.12.05)修改为使用cdn的方式,不用建文件夹导入js。** 12 | 13 | ~~需要echarts的js文件 `echarts.common.min.js` ,在报告文件夹下创建文件夹 `js` ,将该文件放入,生成报告就能看到该报告中的图表~~ 14 | 15 | ## 2017.08 修改为PY3版本 0.9.0 16 | 17 | 基于之前对PY2版本HTMLTestRunner的修改(中文以及美化,[链接点我](http://download.csdn.net/download/huilan_same/9598558)),现在针对PY3做了修改,增加对subTest的支持 18 | 19 | 1. StringIO -> io 20 | 2. 去掉decode 21 | 3. 增加addSubTest() 22 | 23 | ```python 24 | # import StringIO 25 | import io 26 | ... 27 | def startTest(self, test): 28 | TestResult.startTest(self, test) 29 | # just one buffer for both stdout and stderr 30 | # self.outputBuffer = StringIO.StringIO() 31 | self.outputBuffer = io.StringIO() 32 | ... 33 | # add 34 | def addSubTest(self, test, subtest, err): 35 | if err is not None: 36 | if getattr(self, 'failfast', False): 37 | self.stop() 38 | if issubclass(err[0], test.failureException): 39 | self.failure_count += 1 40 | errors = self.failures 41 | errors.append((subtest, self._exc_info_to_string(err, subtest))) 42 | output = self.complete_output() 43 | self.result.append((1, test, output + '\nSubTestCase Failed:\n' + str(subtest), 44 | self._exc_info_to_string(err, subtest))) 45 | if self.verbosity > 1: 46 | sys.stderr.write('F ') 47 | sys.stderr.write(str(subtest)) 48 | sys.stderr.write('\n') 49 | else: 50 | sys.stderr.write('F') 51 | else: 52 | self.error_count += 1 53 | errors = self.errors 54 | errors.append((subtest, self._exc_info_to_string(err, subtest))) 55 | output = self.complete_output() 56 | self.result.append( 57 | (2, test, output + '\nSubTestCase Error:\n' + str(subtest), self._exc_info_to_string(err, subtest))) 58 | if self.verbosity > 1: 59 | sys.stderr.write('E ') 60 | sys.stderr.write(str(subtest)) 61 | sys.stderr.write('\n') 62 | else: 63 | sys.stderr.write('E') 64 | self._mirrorOutput = True 65 | else: 66 | self.subtestlist.append(subtest) 67 | self.subtestlist.append(test) 68 | self.success_count += 1 69 | output = self.complete_output() 70 | self.result.append((0, test, output + '\nSubTestCase Pass:\n' + str(subtest), '')) 71 | if self.verbosity > 1: 72 | sys.stderr.write('ok ') 73 | sys.stderr.write(str(subtest)) 74 | sys.stderr.write('\n') 75 | else: 76 | sys.stderr.write('.') 77 | ... 78 | def run(self, test): 79 | "Run the given test case or test suite." 80 | result = _TestResult(self.verbosity) 81 | test(result) 82 | self.stopTime = datetime.datetime.now() 83 | self.generateReport(test, result) 84 | # print >>>sys.stderr '\nTime Elapsed: %s' % (self.stopTime-self.startTime) 85 | print('\nTime Elapsed: %s' % (self.stopTime-self.startTime), file=sys.stderr) 86 | return result 87 | ... 88 | # if isinstance(o,str): 89 | # # TODO: some problem with 'string_escape': it escape \n and mess up formating 90 | # # uo = unicode(o.encode('string_escape')) 91 | # # uo = o.decode('latin-1') 92 | # uo = o.decode('utf-8') 93 | # else: 94 | # uo = o 95 | # if isinstance(e,str): 96 | # # TODO: some problem with 'string_escape': it escape \n and mess up formating 97 | # # ue = unicode(e.encode('string_escape')) 98 | # # ue = e.decode('latin-1') 99 | # ue = e.decode('utf-8') 100 | # else: 101 | # ue = e 102 | 103 | script = self.REPORT_TEST_OUTPUT_TMPL % dict( 104 | id = tid, 105 | # output = saxutils.escape(uo+ue), 106 | output = saxutils.escape(o+e), 107 | ) 108 | ``` 109 | 110 | 以上代码列出大部分主要修改。 111 | 112 | 如果有任何问题,可在此基础上再次进行修改。 113 | 114 | 关于PY3对subTest的支持,可以查看相关资料,在这里我的处理是:如果一个Case有使用subTest,则将此用例拆分成n个同级子用例,在report中是同级别展示的。 115 | 116 | 117 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huilansame/HTMLTestRunner_PY3/c72fde29c2d53723b60eb950c111687ab296d0d0/__init__.py -------------------------------------------------------------------------------- /img/echarts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huilansame/HTMLTestRunner_PY3/c72fde29c2d53723b60eb950c111687ab296d0d0/img/echarts.png -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from HTMLTestRunner_PY3 import HTMLTestRunner 3 | 4 | 5 | class TestTest(unittest.TestCase): 6 | """ 测试HTMLTestRunner """ 7 | def setUp(self): 8 | self.a = 1 9 | self.b = 2 10 | self.c = [1, 2, 3] 11 | 12 | def test_a_plus_b(self): 13 | """ a + b = 3 这个用例应该通过""" 14 | print('a + b = 3') 15 | self.assertEqual(self.a+self.b, 3) 16 | 17 | def test_a_minus_b(self): 18 | """ a - b = 3 这个用例应该失败 """ 19 | print('a - b = 3') 20 | self.assertEqual(self.a-self.b, 3) 21 | 22 | def test_a_multi_b(self): 23 | """ a * b = 2 这个用例应该成功""" 24 | print('a * b = 2') 25 | self.assertEqual(self.a*self.b, 2) 26 | 27 | def test_a_divide_c(self): 28 | """ a / c = 1 这是个有subTest的用例""" 29 | for i in self.c: 30 | with self.subTest(i=i): 31 | print('a / c = 1') 32 | self.assertEqual(self.a / i, 1) 33 | 34 | def test_a_error_case(self): 35 | """ 除零异常 """ 36 | print('1/0') 37 | self.assertEqual(self.a/0, 1) 38 | 39 | 40 | class ExampleCase1(unittest.TestCase): 41 | """此class包含两个用例:add - ok, minus - FAIL""" 42 | def setUp(self): 43 | self.a = 4 44 | self.b = 3 45 | 46 | def test_add(self): 47 | """用例1,add,此用例成功通过""" 48 | self.assertEqual(self.a + self.b, 7) 49 | 50 | def test_minus(self): 51 | """用例2,minus,此用例执行失败,4-3!=2""" 52 | print('中文方法反反复复凤飞飞反复') 53 | self.assertEqual(self.a - self.b, 2) 54 | 55 | 56 | class ExampleCase2(unittest.TestCase): 57 | """此class包含一个用例:plus - ERROR""" 58 | def setUp(self): 59 | self.a, self.b = 4, 3 60 | 61 | def test_plus(self): 62 | """用例3,plus,此用例执行出错,因为c未定义""" 63 | self.assertEqual(self.a * self.b, c) 64 | 65 | 66 | class ExampleCase3(unittest.TestCase): 67 | """此class包含一个用例:divide - ok""" 68 | def setUp(self): 69 | self.a, self.b = 4, 2 70 | 71 | def test_devide(self): 72 | """用例4,divide,此用例执行成功""" 73 | print('我要打印输出') 74 | self.assertEqual(self.a / self.b, 2) 75 | 76 | 77 | if __name__ == '__main__': 78 | report_title = 'Example用例执行报告' 79 | desc = '用于展示修改样式后的HTMLTestRunner' 80 | report_file = 'ExampleReport.html' 81 | 82 | testsuite = unittest.TestSuite() 83 | testsuite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestTest)) 84 | testsuite.addTest(unittest.TestLoader().loadTestsFromTestCase(ExampleCase1)) 85 | testsuite.addTest(unittest.TestLoader().loadTestsFromTestCase(ExampleCase2)) 86 | testsuite.addTest(unittest.TestLoader().loadTestsFromTestCase(ExampleCase3)) 87 | 88 | with open(report_file, 'wb') as report: 89 | runner = HTMLTestRunner(stream=report, title=report_title, description=desc) 90 | runner.run(testsuite) 91 | --------------------------------------------------------------------------------