前面我们讲到过在做自动化测试或单元测试的时候使用HTMLTestRunner来生成测试报告,并且由于Python2 和 Python3 对于HTMLTestRunner的支持稍微有点差异,所以我们将HTMLTestRunner进行了改造,从而适配Python3,详细改造步骤可以参考:HTMLTestRunner修改成Python3版本
但是改造后的HTMLTestRunner生成的测试报告不是特别的美观,所以我又对HTMLTestRunner进行了进一步的改造,主要是做一些美化。
美化之前的测试报告如下:
美化之后的测试报告如下:
从上面两个报告的对比来看,第二个测试报告是不是更为美观呢?
下面是我改造后的HTMLTestRunner源码:
1 # -*- coding: utf-8 -*- 2 3 """ 4 A TestRunner for use with the Python unit testing framework. It 5 generates a HTML report to show the result at a glance. 6 7 The simplest way to use this is to invoke its main method. E.g. 8 9 import unittest 10 import HTMLTestRunner 11 12 ... define your tests ... 13 14 if __name__ == '__main__': 15 HTMLTestRunner.main() 16 17 18 For more customization options, instantiates a HTMLTestRunner object. 19 HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. 20 21 # output to a file 22 fp = file('my_report.html', 'wb') 23 runner = HTMLTestRunner.HTMLTestRunner( 24 stream=fp, 25 title='My unit test', 26 description='This demonstrates the report output by HTMLTestRunner.' 27 ) 28 29 # Use an external stylesheet. 30 # See the Template_mixin class for more customizable options 31 runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' 32 33 # run the test 34 runner.run(my_test_suite) 35 36 37 ------------------------------------------------------------------------ 38 Copyright (c) 2004-2007, Wai Yip Tung 39 All rights reserved. 40 41 Redistribution and use in source and binary forms, with or without 42 modification, are permitted provided that the following conditions are 43 met: 44 45 * Redistributions of source code must retain the above copyright notice, 46 this list of conditions and the following disclaimer. 47 * Redistributions in binary form must reproduce the above copyright 48 notice, this list of conditions and the following disclaimer in the 49 documentation and/or other materials provided with the distribution. 50 * Neither the name Wai Yip Tung nor the names of its contributors may be 51 used to endorse or promote products derived from this software without 52 specific prior written permission. 53 54 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 55 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 56 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 57 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 58 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 59 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 60 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 61 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 62 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 63 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 64 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 65 """ 66 67 # URL: http://tungwaiyip.info/software/HTMLTestRunner.html 68 69 __author__ = "Wai Yip Tung" 70 __version__ = "0.8.2.3" 71 72 73 """ 74 Change History 75 Version 0.8.2.1 -Findyou 76 * 改为支持python3 77 78 Version 0.8.2.1 -Findyou 79 * 支持中文,汉化 80 * 调整样式,美化(需要连入网络,使用的百度的Bootstrap.js) 81 * 增加 通过分类显示、测试人员、通过率的展示 82 * 优化“详细”与“收起”状态的变换 83 * 增加返回顶部的锚点 84 85 Version 0.8.2 86 * Show output inline instead of popup window (Viorel Lupu). 87 88 Version in 0.8.1 89 * Validated XHTML (Wolfgang Borgert). 90 * Added description of test classes and test cases. 91 92 Version in 0.8.0 93 * Define Template_mixin class for customization. 94 * Workaround a IE 6 bug that it does not treat <script> block as CDATA. 95 96 Version in 0.7.1 97 * Back port to Python 2.3 (Frank Horowitz). 98 * Fix missing scroll bars in detail log (Podi). 99 """ 100 101 # TODO: color stderr 102 # TODO: simplify javascript using ,ore than 1 class in the class attribute? 103 104 import datetime 105 import io 106 import sys 107 import time 108 import unittest 109 from xml.sax import saxutils 110 import sys 111 112 # ------------------------------------------------------------------------ 113 # The redirectors below are used to capture output during testing. Output 114 # sent to sys.stdout and sys.stderr are automatically captured. However 115 # in some cases sys.stdout is already cached before HTMLTestRunner is 116 # invoked (e.g. calling logging.basicConfig). In order to capture those 117 # output, use the redirectors for the cached stream. 118 # 119 # e.g. 120 # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) 121 # >>> 122 123 class OutputRedirector(object): 124 """ Wrapper to redirect stdout or stderr """ 125 def __init__(self, fp): 126 self.fp = fp 127 128 def write(self, s): 129 self.fp.write(s) 130 131 def writelines(self, lines): 132 self.fp.writelines(lines) 133 134 def flush(self): 135 self.fp.flush() 136 137 stdout_redirector = OutputRedirector(sys.stdout) 138 stderr_redirector = OutputRedirector(sys.stderr) 139 140 # ---------------------------------------------------------------------- 141 # Template 142 143 class Template_mixin(object): 144 """ 145 Define a HTML template for report customerization and generation. 146 147 Overall structure of an HTML report 148 149 HTML 150 +------------------------+ 151 |<html> | 152 | <head> | 153 | | 154 | STYLESHEET | 155 | +----------------+ | 156 | | | | 157 | +----------------+ | 158 | | 159 | </head> | 160 | | 161 | <body> | 162 | | 163 | HEADING | 164 | +----------------+ | 165 | | | | 166 | +----------------+ | 167 | | 168 | REPORT | 169 | +----------------+ | 170 | | | | 171 | +----------------+ | 172 | | 173 | ENDING | 174 | +----------------+ | 175 | | | | 176 | +----------------+ | 177 | | 178 | </body> | 179 |</html> | 180 +------------------------+ 181 """ 182 183 STATUS = { 184 0: '通过', 185 1: '失败', 186 2: '错误', 187 } 188 # 默认测试标题 189 DEFAULT_TITLE = 'API自动化测试报告' 190 DEFAULT_DESCRIPTION = '' 191 # 默认测试人员 192 DEFAULT_TESTER = 'lwjnicole' 193 194 # ------------------------------------------------------------------------ 195 # HTML Template 196 197 HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> 198 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 199 <html xmlns="http://www.w3.org/1999/xhtml"> 200 <head> 201 <title>%(title)s</title> 202 <meta name="generator" content="%(generator)s"/> 203 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 204 <link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> 205 <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> 206 <script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script> 207 %(stylesheet)s 208 </head> 209 <body > 210 <script language="javascript" type="text/javascript"> 211 output_list = Array(); 212 213 /*level 调整增加只显示通过用例的分类 --Adil 214 0:Summary //all hiddenRow 215 1:Failed //pt hiddenRow, ft none 216 2:Pass //pt none, ft hiddenRow 217 3:Error // pt hiddenRow, ft none 218 4:All //pt none, ft none 219 下面设置 按钮展开逻辑 --Yang Yao Jun 220 */ 221 function showCase(level) { 222 trs = document.getElementsByTagName("tr"); 223 for (var i = 0; i < trs.length; i++) { 224 tr = trs[i]; 225 id = tr.id; 226 if (id.substr(0,2) == 'ft') { 227 if (level == 2 || level == 0 ) { 228 tr.className = 'hiddenRow'; 229 } 230 else { 231 tr.className = ''; 232 } 233 } 234 if (id.substr(0,2) == 'pt') { 235 if (level < 2 || level ==3 ) { 236 tr.className = 'hiddenRow'; 237 } 238 else { 239 tr.className = ''; 240 } 241 } 242 } 243 244 //加入【详细】切换文字变化 --Findyou 245 detail_class=document.getElementsByClassName('detail'); 246 //console.log(detail_class.length) 247 if (level == 3) { 248 for (var i = 0; i < detail_class.length; i++){ 249 detail_class[i].innerHTML="收起" 250 } 251 } 252 else{ 253 for (var i = 0; i < detail_class.length; i++){ 254 detail_class[i].innerHTML="详细" 255 } 256 } 257 } 258 259 function showClassDetail(cid, count) { 260 var id_list = Array(count); 261 var toHide = 1; 262 for (var i = 0; i < count; i++) { 263 //ID修改 点 为 下划线 -Findyou 264 tid0 = 't' + cid.substr(1) + '_' + (i+1); 265 tid = 'f' + tid0; 266 tr = document.getElementById(tid); 267 if (!tr) { 268 tid = 'p' + tid0; 269 tr = document.getElementById(tid); 270 } 271 id_list[i] = tid; 272 if (tr.className) { 273 toHide = 0; 274 } 275 } 276 for (var i = 0; i < count; i++) { 277 tid = id_list[i]; 278 //修改点击无法收起的BUG,加入【详细】切换文字变化 --Findyou 279 if (toHide) { 280 document.getElementById(tid).className = 'hiddenRow'; 281 document.getElementById(cid).innerText = "详细" 282 } 283 else { 284 document.getElementById(tid).className = ''; 285 document.getElementById(cid).innerText = "收起" 286 } 287 } 288 } 289 290 function html_escape(s) { 291 s = s.replace(/&/g,'&'); 292 s = s.replace(/</g,'<'); 293 s = s.replace(/>/g,'>'); 294 return s; 295 } 296 </script> 297 %(heading)s 298 %(report)s 299 %(ending)s 300 301 </body> 302 </html> 303 """ 304 # variables: (title, generator, stylesheet, heading, report, ending) 305 306 307 # ------------------------------------------------------------------------ 308 # Stylesheet 309 # 310 # alternatively use a <link> for external style sheet, e.g. 311 # <link rel="stylesheet" href="$url" type="text/css"> 312 313 STYLESHEET_TMPL = """ 314 <style type="text/css" media="screen"> 315 body { font-family: Microsoft YaHei,Tahoma,arial,helvetica,sans-serif;padding: 20px; font-size: 80%; } 316 table { font-size: 100%; } 317 318 /* -- heading ---------------------------------------------------------------------- */ 319 .heading { 320 margin-top: 0ex; 321 margin-bottom: 1ex; 322 } 323 324 .heading .description { 325 margin-top: 4ex; 326 margin-bottom: 6ex; 327 } 328 329 /* -- report ------------------------------------------------------------------------ */ 330 #total_row { font-weight: bold; } 331 .passCase { color: #5cb85c; } 332 .failCase { color: #d9534f; font-weight: bold; } 333 .errorCase { color: #f0ad4e; font-weight: bold; } 334 .hiddenRow { display: none; } 335 .testcase { margin-left: 2em; } 336 </style> 337 """ 338 339 # ------------------------------------------------------------------------ 340 # Heading 341 # 342 343 HEADING_TMPL = """<div class='heading'> 344 <h1 style="font-family: Microsoft YaHei">%(title)s</h1> 345 %(parameters)s 346 <p class='description'>%(description)s</p> 347 </div> 348 349 """ # variables: (title, parameters, description) 350 351 HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s : </strong> %(value)s</p> 352 """ # variables: (name, value) 353 354 355 356 # ------------------------------------------------------------------------ 357 # Report 358 # 359 # 汉化,加美化效果 --Yang Yao Jun 360 # 361 # 这里涉及到了 Bootstrap 前端技术,Bootstrap 按钮 资料介绍详见:http://www.runoob.com/bootstrap/bootstrap-buttons.html 362 # 363 REPORT_TMPL = """ 364 <p id='show_detail_line'> 365 <a class="btn btn-primary" href='javascript:showCase(0)'>通过率 [%(passrate)s ]</a> 366 <a class="btn btn-success" href='javascript:showCase(2)'>通过[ %(Pass)s ]</a> 367 <a class="btn btn-warning" href='javascript:showCase(3)'>错误[ %(error)s ]</a> 368 <a class="btn btn-danger" href='javascript:showCase(1)'>失败[ %(fail)s ]</a> 369 <a class="btn btn-info" href='javascript:showCase(4)'>所有[ %(count)s ]</a> 370 </p> 371 <table id='result_table' class="table table-condensed table-bordered table-hover"> 372 <colgroup> 373 <col align='left' /> 374 <col align='right' /> 375 <col align='right' /> 376 <col align='right' /> 377 <col align='right' /> 378 <col align='right' /> 379 </colgroup> 380 <tr id='header_row' class="text-center success" style="font-weight: bold;font-size: 14px;"> 381 <td>用例集/测试用例</td> 382 <td>总计</td> 383 <td>通过</td> 384 <td>错误</td> 385 <td>失败</td> 386 <td>详细</td> 387 </tr> 388 %(test_list)s 389 <tr id='total_row' class="text-center active"> 390 <td>总计</td> 391 <td>%(count)s</td> 392 <td>%(Pass)s</td> 393 <td>%(error)s</td> 394 <td>%(fail)s</td> 395 <td>通过率:%(passrate)s</td> 396 </tr> 397 </table> 398 """ # variables: (test_list, count, Pass, fail, error ,passrate) 399 400 REPORT_CLASS_TMPL = r""" 401 <tr class='%(style)s warning'> 402 <td>%(desc)s</td> 403 <td class="text-center">%(count)s</td> 404 <td class="text-center">%(Pass)s</td> 405 <td class="text-center">%(error)s</td> 406 <td class="text-center">%(fail)s</td> 407 <td class="text-center"><a href="javascript:showClassDetail('%(cid)s',%(count)s)" class="detail" id='%(cid)s'>详细</a></td> 408 </tr> 409 """ # variables: (style, desc, count, Pass, fail, error, cid) 410 411 #失败 的样式,去掉原来JS效果,美化展示效果 -Findyou 412 REPORT_TEST_WITH_OUTPUT_TMPL = r""" 413 <tr id='%(tid)s' class='%(Class)s'> 414 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 415 <td colspan='5' align='center'> 416 <!--默认收起错误信息 -Findyou 417 <button id='btn_%(tid)s' type="button" class="btn btn-danger btn-xs collapsed" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button> 418 <div id='div_%(tid)s' class="collapse"> --> 419 420 <!-- 默认展开错误信息 -Findyou --> 421 <button id='btn_%(tid)s' type="button" class="btn btn-danger btn-xs" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button> 422 <div id='div_%(tid)s' class="collapse in" style='text-align: left; color:red;cursor:pointer'> 423 <pre> 424 %(script)s 425 </pre> 426 </div> 427 </td> 428 </tr> 429 """ # variables: (tid, Class, style, desc, status) 430 431 # 通过 的样式,加标签效果 -Findyou 432 REPORT_TEST_NO_OUTPUT_TMPL = r""" 433 <tr id='%(tid)s' class='%(Class)s'> 434 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 435 <td colspan='5' align='center'><span class="label label-success success">%(status)s</span></td> 436 </tr> 437 """ # variables: (tid, Class, style, desc, status) 438 439 REPORT_TEST_OUTPUT_TMPL = r""" 440 %(id)s: %(output)s 441 """ # variables: (id, output) 442 443 # ------------------------------------------------------------------------ 444 # ENDING 445 # 446 # 增加返回顶部按钮 --Findyou 447 ENDING_TMPL = """<div id='ending'> </div> 448 <div style=" position:fixed;right:50px; bottom:30px; 20px; height:20px;cursor:pointer"> 449 <a href="#"><span class="glyphicon glyphicon-eject" style = "font-size:30px;" aria-hidden="true"> 450 </span></a></div> 451 """ 452 453 # -------------------- The end of the Template class ------------------- 454 455 456 TestResult = unittest.TestResult 457 458 class _TestResult(TestResult): 459 # note: _TestResult is a pure representation of results. 460 # It lacks the output and reporting ability compares to unittest._TextTestResult. 461 462 def __init__(self, verbosity=1): 463 TestResult.__init__(self) 464 self.stdout0 = None 465 self.stderr0 = None 466 self.success_count = 0 467 self.failure_count = 0 468 self.error_count = 0 469 self.verbosity = verbosity 470 471 # result is a list of result in 4 tuple 472 # ( 473 # result code (0: success; 1: fail; 2: error), 474 # TestCase object, 475 # Test output (byte string), 476 # stack trace, 477 # ) 478 self.result = [] 479 #增加一个测试通过率 --Findyou 480 self.passrate=float(0) 481 482 483 def startTest(self, test): 484 TestResult.startTest(self, test) 485 # just one buffer for both stdout and stderr 486 self.outputBuffer = io.StringIO() 487 stdout_redirector.fp = self.outputBuffer 488 stderr_redirector.fp = self.outputBuffer 489 self.stdout0 = sys.stdout 490 self.stderr0 = sys.stderr 491 sys.stdout = stdout_redirector 492 sys.stderr = stderr_redirector 493 494 495 def complete_output(self): 496 """ 497 Disconnect output redirection and return buffer. 498 Safe to call multiple times. 499 """ 500 if self.stdout0: 501 sys.stdout = self.stdout0 502 sys.stderr = self.stderr0 503 self.stdout0 = None 504 self.stderr0 = None 505 return self.outputBuffer.getvalue() 506 507 508 def stopTest(self, test): 509 # Usually one of addSuccess, addError or addFailure would have been called. 510 # But there are some path in unittest that would bypass this. 511 # We must disconnect stdout in stopTest(), which is guaranteed to be called. 512 self.complete_output() 513 514 515 def addSuccess(self, test): 516 self.success_count += 1 517 TestResult.addSuccess(self, test) 518 output = self.complete_output() 519 self.result.append((0, test, output, '')) 520 if self.verbosity > 1: 521 sys.stderr.write('ok ') 522 sys.stderr.write(str(test)) 523 sys.stderr.write(' ') 524 else: 525 sys.stderr.write('.') 526 527 def addError(self, test, err): 528 self.error_count += 1 529 TestResult.addError(self, test, err) 530 _, _exc_str = self.errors[-1] 531 output = self.complete_output() 532 self.result.append((2, test, output, _exc_str)) 533 if self.verbosity > 1: 534 sys.stderr.write('E ') 535 sys.stderr.write(str(test)) 536 sys.stderr.write(' ') 537 else: 538 sys.stderr.write('E') 539 540 def addFailure(self, test, err): 541 self.failure_count += 1 542 TestResult.addFailure(self, test, err) 543 _, _exc_str = self.failures[-1] 544 output = self.complete_output() 545 self.result.append((1, test, output, _exc_str)) 546 if self.verbosity > 1: 547 sys.stderr.write('F ') 548 sys.stderr.write(str(test)) 549 sys.stderr.write(' ') 550 else: 551 sys.stderr.write('F') 552 553 554 class HTMLTestRunner(Template_mixin): 555 """ 556 """ 557 def __init__(self, stream=sys.stdout, verbosity=1,title=None,description=None,tester=None): 558 self.stream = stream 559 self.verbosity = verbosity 560 if title is None: 561 self.title = self.DEFAULT_TITLE 562 else: 563 self.title = title 564 if description is None: 565 self.description = self.DEFAULT_DESCRIPTION 566 else: 567 self.description = description 568 if tester is None: 569 self.tester = self.DEFAULT_TESTER 570 else: 571 self.tester = tester 572 573 self.startTime = datetime.datetime.now() 574 575 576 def run(self, test): 577 "Run the given test case or test suite." 578 result = _TestResult(self.verbosity) 579 test(result) 580 self.stopTime = datetime.datetime.now() 581 self.generateReport(test, result) 582 print(' Time Elapsed: %s' % (self.stopTime-self.startTime), file=sys.stderr) 583 return result 584 585 586 def sortResult(self, result_list): 587 # unittest does not seems to run in any particular order. 588 # Here at least we want to group them together by class. 589 rmap = {} 590 classes = [] 591 for n,t,o,e in result_list: 592 cls = t.__class__ 593 if cls not in rmap: 594 rmap[cls] = [] 595 classes.append(cls) 596 rmap[cls].append((n,t,o,e)) 597 r = [(cls, rmap[cls]) for cls in classes] 598 return r 599 600 #替换测试结果status为通过率 --Findyou 601 def getReportAttributes(self, result): 602 """ 603 Return report attributes as a list of (name, value). 604 Override this to add custom attributes. 605 """ 606 startTime = str(self.startTime)[:19] 607 duration = str(self.stopTime - self.startTime) 608 status = [] 609 status.append('共 %s' % (result.success_count + result.failure_count + result.error_count)) 610 if result.success_count: status.append('通过 %s' % result.success_count) 611 if result.failure_count: status.append('失败 %s' % result.failure_count) 612 if result.error_count: status.append('错误 %s' % result.error_count ) 613 if status: 614 status = ','.join(status) 615 self.passrate = str("%.2f%%" % (float(result.success_count) / float(result.success_count + result.failure_count + result.error_count) * 100)) 616 else: 617 status = 'none' 618 return [ 619 ('测试人员', self.tester), 620 ('开始时间',startTime), 621 ('合计耗时',duration), 622 ('测试结果',status + ",通过率= "+self.passrate), 623 ] 624 625 626 def generateReport(self, test, result): 627 report_attrs = self.getReportAttributes(result) 628 generator = 'HTMLTestRunner %s' % __version__ 629 stylesheet = self._generate_stylesheet() 630 heading = self._generate_heading(report_attrs) 631 report = self._generate_report(result) 632 ending = self._generate_ending() 633 output = self.HTML_TMPL % dict( 634 title = saxutils.escape(self.title), 635 generator = generator, 636 stylesheet = stylesheet, 637 heading = heading, 638 report = report, 639 ending = ending, 640 ) 641 self.stream.write(output.encode('utf8')) 642 643 644 def _generate_stylesheet(self): 645 return self.STYLESHEET_TMPL 646 647 #增加Tester显示 -Findyou 648 def _generate_heading(self, report_attrs): 649 a_lines = [] 650 for name, value in report_attrs: 651 line = self.HEADING_ATTRIBUTE_TMPL % dict( 652 name = saxutils.escape(name), 653 value = saxutils.escape(value), 654 ) 655 a_lines.append(line) 656 heading = self.HEADING_TMPL % dict( 657 title = saxutils.escape(self.title), 658 parameters = ''.join(a_lines), 659 description = saxutils.escape(self.description), 660 tester= saxutils.escape(self.tester), 661 ) 662 return heading 663 664 #生成报告 --Findyou添加注释 665 def _generate_report(self, result): 666 rows = [] 667 sortedResult = self.sortResult(result.result) 668 for cid, (cls, cls_results) in enumerate(sortedResult): 669 # subtotal for a class 670 np = nf = ne = 0 671 for n,t,o,e in cls_results: 672 if n == 0: np += 1 673 elif n == 1: nf += 1 674 else: ne += 1 675 676 # format class description 677 if cls.__module__ == "__main__": 678 name = cls.__name__ 679 else: 680 name = "%s.%s" % (cls.__module__, cls.__name__) 681 doc = cls.__doc__ and cls.__doc__.split(" ")[0] or "" 682 desc = doc and '%s: %s' % (name, doc) or name 683 684 row = self.REPORT_CLASS_TMPL % dict( 685 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 686 desc = desc, 687 count = np+nf+ne, 688 Pass = np, 689 fail = nf, 690 error = ne, 691 cid = 'c%s' % (cid+1), 692 ) 693 rows.append(row) 694 695 for tid, (n,t,o,e) in enumerate(cls_results): 696 self._generate_report_test(rows, cid, tid, n, t, o, e) 697 698 report = self.REPORT_TMPL % dict( 699 test_list = ''.join(rows), 700 count = str(result.success_count+result.failure_count+result.error_count), 701 Pass = str(result.success_count), 702 fail = str(result.failure_count), 703 error = str(result.error_count), 704 passrate =self.passrate, 705 ) 706 return report 707 708 709 def _generate_report_test(self, rows, cid, tid, n, t, o, e): 710 # e.g. 'pt1.1', 'ft1.1', etc 711 has_output = bool(o or e) 712 # ID修改点为下划线,支持Bootstrap折叠展开特效 - Findyou 713 tid = (n == 0 and 'p' or 'f') + 't%s_%s' % (cid+1,tid+1) 714 name = t.id().split('.')[-1] 715 doc = t.shortDescription() or "" 716 desc = doc and ('%s: %s' % (name, doc)) or name 717 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 718 719 # utf-8 支持中文 - Findyou 720 # o and e should be byte string because they are collected from stdout and stderr? 721 if isinstance(o, str): 722 # TODO: some problem with 'string_escape': it escape and mess up formating 723 # uo = unicode(o.encode('string_escape')) 724 # uo = o.decode('latin-1') 725 uo = o 726 else: 727 uo = o 728 if isinstance(e, str): 729 # TODO: some problem with 'string_escape': it escape and mess up formating 730 # ue = unicode(e.encode('string_escape')) 731 # ue = e.decode('latin-1') 732 ue = e 733 else: 734 ue = e 735 736 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 737 id = tid, 738 output = saxutils.escape(uo+ue), 739 ) 740 741 row = tmpl % dict( 742 tid = tid, 743 Class = (n == 0 and 'hiddenRow' or 'none'), 744 style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'), 745 desc = desc, 746 script = script, 747 status = self.STATUS[n], 748 ) 749 rows.append(row) 750 if not has_output: 751 return 752 753 def _generate_ending(self): 754 return self.ENDING_TMPL 755 756 757 ############################################################################## 758 # Facilities for running tests from the command line 759 ############################################################################## 760 761 # Note: Reuse unittest.TestProgram to launch test. In the future we may 762 # build our own launcher to support more specific command line 763 # parameters like test title, CSS, etc. 764 class TestProgram(unittest.TestProgram): 765 """ 766 A variation of the unittest.TestProgram. Please refer to the base 767 class for command line parameters. 768 """ 769 def runTests(self): 770 # Pick HTMLTestRunner as the default test runner. 771 # base class's testRunner parameter is not useful because it means 772 # we have to instantiate HTMLTestRunner before we know self.verbosity. 773 if self.testRunner is None: 774 self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 775 unittest.TestProgram.runTests(self) 776 777 main = TestProgram 778 779 ############################################################################## 780 # Executing this module from the command line 781 ############################################################################## 782 783 if __name__ == "__main__": 784 main(module=None)
如何使用呢?
使用方法:
1.将上面的源码复制一份,将文件命名为HTMLTestRunner.py ,然后将HTMLTestRunner.py 文件放到Python安装目录的Lib目录下:
2.导入HTMLTestRunner.py模块,运行以下代码,即可生成美化后的测试报告:
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 # @Time : 2019/12/29 15:41 4 # @Author : lwjnicole 5 # @File : run_tests.py 6 7 import unittest 8 import time 9 from HTMLTestRunner import HTMLTestRunner 10 11 testcase_dir = './interface/configcenter' 12 discover = unittest.defaultTestLoader.discover(testcase_dir, '*_test.py') 13 14 if __name__ == '__main__': 15 now = time.strftime("%Y-%m-%d %H_%M_%S") 16 filename = './report/' + now + '_result.html' 17 with open(filename, 'wb') as f: 18 runner = HTMLTestRunner(stream=f, title='接口自动化测试报告', description='营销线接口') 19 runner.run(discover)
testcase_dir:指定需要执行的测试用例目录路径;
HTMLTestRunner(stream=f, title='接口自动化测试报告', description='营销线接口'):还可以传入tester='lwjnicole',tester表示测试人员;
执行完成之后,就会在上面自定义report目录下生成指定格式文件名的html测试报告啦!