簡單目錄層級分4層(效果見下圖)
driver層: 驅動層,放置各個瀏覽器驅動版本,做ui自動化需要考慮兼容性(類型是否支持谷歌,火狐,ie等,支持哪幾個谷歌版本等等)
testcases層: 用例層,放置UI自動化腳本,腳本命名一般以test_開頭
report層: 報告層,放置UI自動化運行結果報告,一般以html格式生成
utils層: 工具層,放置工具類,類似下圖中的HTMLTestRunner文件(生成結果報告類),還可以放數據庫操作、時間操作、字符串處理、文件處理等等類
run_all_case.py: 主入口,執行UI自動化,只需要執行這個類,就會去獲取所有testcases層的用例,然后運行結果保存在report層
run_all_case.py:具體代碼如下
# -*- coding:utf-8 -*-
import unittest
import os
from utils.HTMLTestRunnerForPy3 import HTMLTestRunner
from datetime import datetime
if __name__ == "__main__":
#挑選用例,pattern='test_*.py'表示添加test_開頭的py文件
casePath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testcases')
discover = unittest.defaultTestLoader.discover(
start_dir=casePath,
pattern='test_*.py'
)
#指定生成報告地址
reportPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'reports')
reportName = datetime.now().strftime("%Y%m%d%H%M%S") + '.html'
reportFile = os.path.join(reportPath, reportName)
fp = open(reportFile, 'wb')
# 運行用例
runner = HTMLTestRunner(
stream=fp,
# 生成的html報告標題
title='銀行UI自動化測試報告',
# 1是粗略的報告,2是詳細的報告
verbosity=2,
# 生成的html描述
description='銀行UI自動化測試報告'
)
runner.run(discover)
# 創建的文件都需要關閉
fp.close()
test_aaa.py:具體代碼如下
# -*- coding:utf-8 -*- import unittest from selenium import webdriver import time #QingQing類的名字任意命名,但命名()里的unittest.TestCase就是去繼承這個類,類的作用就是可以使runner.run識別 class QingQing(unittest.TestCase): #unittest.TestCase類定義的setUpClass和tearDownClass方法前一定要加@classmethod, #setUpClass在這個類里面是第一個執行的方法 #tearDownClass在這個類里面是最后一個執行的方法 #中間的執行順序是通過字符的大小進行順序執行,命名必須test_開頭 #打開瀏覽器,獲取配置 @classmethod def setUpClass(self): # 實例化ChromeOptions options = webdriver.ChromeOptions() # 關閉瀏覽器提示信息 options.add_argument('disable-infobars') # 瀏覽器全屏 options.add_argument('start-fullscreen') driverpath = r'D:\angel\angelauto\littlebee1\driver\chromedriver.exe' #driver驅動獲取后可以被其他方法調用 self.driver = webdriver.Chrome(driverpath, options=options) def test_01_search_baidu(self): # 訪問百度首頁 self.driver.get(r"http://www.baidu.com") # 百度輸入框輸入 self.driver.find_element_by_id("kw").send_keys("懶勺") # 點百度一下 self.driver.find_element_by_id("su").click() #等待時間只是為了讓你可以看到目前效果,可以省略 time.sleep(2) #執行商品收費功能 def test_02_search_qq_news(self): # 訪問qq首頁 self.driver.get(r"http://www.qq.com") # 點新聞鏈接 self.driver.find_element_by_xpath("//a[text()='新聞']").click() # 等待時間只是為了讓你可以看到目前效果,可以省略 time.sleep(3) #退出瀏覽器 @classmethod def tearDownClass(self): self.driver.quit() if __name__ == "__main__": unittest.main()
HTMLTestRunnerForPy3.py:具體代碼如下(第三方工具類,直接使用即可)

1 """ 2 A TestRunner for use with the Python unit testing framework. It 3 generates a HTML report to show the result at a glance. 4 5 The simplest way to use this is to invoke its main method. E.g. 6 7 import unittest 8 import HTMLTestRunner 9 10 ... define your tests ... 11 12 if __name__ == '__main__': 13 HTMLTestRunner.main() 14 15 16 For more customization options, instantiates a HTMLTestRunner object. 17 HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. 18 19 # output to a file 20 fp = file('my_report.html', 'wb') 21 runner = HTMLTestRunner.HTMLTestRunner( 22 stream=fp, 23 title='My unit test', 24 description='This demonstrates the report output by HTMLTestRunner.' 25 ) 26 27 # Use an external stylesheet. 28 # See the Template_mixin class for more customizable options 29 runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' 30 31 # run the test 32 runner.run(my_test_suite) 33 34 35 ------------------------------------------------------------------------ 36 Copyright (c) 2004-2007, Wai Yip Tung 37 All rights reserved. 38 39 Redistribution and use in source and binary forms, with or without 40 modification, are permitted provided that the following conditions are 41 met: 42 43 * Redistributions of source code must retain the above copyright notice, 44 this list of conditions and the following disclaimer. 45 * Redistributions in binary form must reproduce the above copyright 46 notice, this list of conditions and the following disclaimer in the 47 documentation and/or other materials provided with the distribution. 48 * Neither the name Wai Yip Tung nor the names of its contributors may be 49 used to endorse or promote products derived from this software without 50 specific prior written permission. 51 52 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 53 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 54 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 55 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 56 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 57 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 58 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 59 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 60 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 61 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 62 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 63 """ 64 65 # URL: http://tungwaiyip.info/software/HTMLTestRunner.html 66 67 __author__ = "Wai Yip Tung" 68 __version__ = "0.8.2" 69 70 71 """ 72 Change History 73 74 Version 0.8.2 75 * Show output inline instead of popup window (Viorel Lupu). 76 77 Version in 0.8.1 78 * Validated XHTML (Wolfgang Borgert). 79 * Added description of test classes and test cases. 80 81 Version in 0.8.0 82 * Define Template_mixin class for customization. 83 * Workaround a IE 6 bug that it does not treat <script> block as CDATA. 84 85 Version in 0.7.1 86 * Back port to Python 2.3 (Frank Horowitz). 87 * Fix missing scroll bars in detail logs (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 import io 95 import sys 96 import time 97 import unittest 98 from xml.sax import saxutils 99 100 101 # ------------------------------------------------------------------------ 102 # The redirectors below are used to capture output during testing. Output 103 # sent to sys.stdout and sys.stderr are automatically captured. However 104 # in some cases sys.stdout is already cached before HTMLTestRunner is 105 # invoked (e.g. calling logging.basicConfig). In order to capture those 106 # output, use the redirectors for the cached stream. 107 # 108 # e.g. 109 # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) 110 # >>> 111 112 class OutputRedirector(object): 113 """ Wrapper to redirect stdout or stderr """ 114 def __init__(self, fp): 115 self.fp = fp 116 117 def write(self, s): 118 # self.fp.write(s) 119 self.fp.write(bytes(s, 'UTF-8')) 120 121 def writelines(self, lines): 122 self.fp.writelines(lines) 123 124 def flush(self): 125 self.fp.flush() 126 127 stdout_redirector = OutputRedirector(sys.stdout) 128 stderr_redirector = OutputRedirector(sys.stderr) 129 130 131 132 # ---------------------------------------------------------------------- 133 # Template 134 135 class Template_mixin(object): 136 """ 137 Define a HTML template for report customerization and generation. 138 139 Overall structure of an HTML report 140 141 HTML 142 +------------------------+ 143 |<html> | 144 | <head> | 145 | | 146 | STYLESHEET | 147 | +----------------+ | 148 | | | | 149 | +----------------+ | 150 | | 151 | </head> | 152 | | 153 | <body> | 154 | | 155 | HEADING | 156 | +----------------+ | 157 | | | | 158 | +----------------+ | 159 | | 160 | REPORT | 161 | +----------------+ | 162 | | | | 163 | +----------------+ | 164 | | 165 | ENDING | 166 | +----------------+ | 167 | | | | 168 | +----------------+ | 169 | | 170 | </body> | 171 |</html> | 172 +------------------------+ 173 """ 174 175 STATUS = { 176 0: 'pass', 177 1: 'fail', 178 2: 'error', 179 } 180 181 DEFAULT_TITLE = 'Unit Test Report' 182 DEFAULT_DESCRIPTION = '' 183 184 # ------------------------------------------------------------------------ 185 # HTML Template 186 187 HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> 188 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 189 <html xmlns="http://www.w3.org/1999/xhtml"> 190 <head> 191 <title>%(title)s</title> 192 <meta name="generator" content="%(generator)s"/> 193 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 194 %(stylesheet)s 195 </head> 196 <body> 197 <script language="javascript" type="text/javascript"><!-- 198 output_list = Array(); 199 200 /* level - 0:Summary; 1:Failed; 2:All */ 201 function showCase(level) { 202 trs = document.getElementsByTagName("tr"); 203 for (var i = 0; i < trs.length; i++) { 204 tr = trs[i]; 205 id = tr.id; 206 if (id.substr(0,2) == 'ft') { 207 if (level < 1) { 208 tr.className = 'hiddenRow'; 209 } 210 else { 211 tr.className = ''; 212 } 213 } 214 if (id.substr(0,2) == 'pt') { 215 if (level > 1) { 216 tr.className = ''; 217 } 218 else { 219 tr.className = 'hiddenRow'; 220 } 221 } 222 } 223 } 224 225 226 function showClassDetail(cid, count) { 227 var id_list = Array(count); 228 var toHide = 1; 229 for (var i = 0; i < count; i++) { 230 tid0 = 't' + cid.substr(1) + '.' + (i+1); 231 tid = 'f' + tid0; 232 tr = document.getElementById(tid); 233 if (!tr) { 234 tid = 'p' + tid0; 235 tr = document.getElementById(tid); 236 } 237 id_list[i] = tid; 238 if (tr.className) { 239 toHide = 0; 240 } 241 } 242 for (var i = 0; i < count; i++) { 243 tid = id_list[i]; 244 if (toHide) { 245 document.getElementById('div_'+tid).style.display = 'none' 246 document.getElementById(tid).className = 'hiddenRow'; 247 } 248 else { 249 document.getElementById(tid).className = ''; 250 } 251 } 252 } 253 254 255 function showTestDetail(div_id){ 256 var details_div = document.getElementById(div_id) 257 var displayState = details_div.style.display 258 // alert(displayState) 259 if (displayState != 'block' ) { 260 displayState = 'block' 261 details_div.style.display = 'block' 262 } 263 else { 264 details_div.style.display = 'none' 265 } 266 } 267 268 269 function html_escape(s) { 270 s = s.replace(/&/g,'&'); 271 s = s.replace(/</g,'<'); 272 s = s.replace(/>/g,'>'); 273 return s; 274 } 275 276 /* obsoleted by detail in <div> 277 function showOutput(id, name) { 278 var w = window.open("", //url 279 name, 280 "resizable,scrollbars,status,width=800,height=450"); 281 d = w.document; 282 d.write("<pre>"); 283 d.write(html_escape(output_list[id])); 284 d.write("\n"); 285 d.write("<a href='javascript:window.close()'>close</a>\n"); 286 d.write("</pre>\n"); 287 d.close(); 288 } 289 */ 290 --></script> 291 292 %(heading)s 293 %(report)s 294 %(ending)s 295 296 </body> 297 </html> 298 """ 299 # variables: (title, generator, stylesheet, heading, report, ending) 300 301 302 # ------------------------------------------------------------------------ 303 # Stylesheet 304 # 305 # alternatively use a <link> for external style sheet, e.g. 306 # <link rel="stylesheet" href="$url" type="text/css"> 307 308 STYLESHEET_TMPL = """ 309 <style type="text/css" media="screen"> 310 body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; } 311 table { font-size: 100%; } 312 pre { } 313 314 /* -- heading ---------------------------------------------------------------------- */ 315 h1 { 316 font-size: 16pt; 317 color: gray; 318 } 319 .heading { 320 margin-top: 0ex; 321 margin-bottom: 1ex; 322 } 323 324 .heading .attribute { 325 margin-top: 1ex; 326 margin-bottom: 0; 327 } 328 329 .heading .description { 330 margin-top: 4ex; 331 margin-bottom: 6ex; 332 } 333 334 /* -- css div popup ------------------------------------------------------------------------ */ 335 a.popup_link { 336 } 337 338 a.popup_link:hover { 339 color: red; 340 } 341 342 .popup_window { 343 display: none; 344 position: relative; 345 left: 0px; 346 top: 0px; 347 /*border: solid #627173 1px; */ 348 padding: 10px; 349 background-color: #E6E6D6; 350 font-family: "Lucida Console", "Courier New", Courier, monospace; 351 text-align: left; 352 font-size: 8pt; 353 width: 500px; 354 } 355 356 } 357 /* -- report ------------------------------------------------------------------------ */ 358 #show_detail_line { 359 margin-top: 3ex; 360 margin-bottom: 1ex; 361 } 362 #result_table { 363 width: 80%; 364 border-collapse: collapse; 365 border: 1px solid #777; 366 } 367 #header_row { 368 font-weight: bold; 369 color: white; 370 background-color: #777; 371 } 372 #result_table td { 373 border: 1px solid #777; 374 padding: 2px; 375 } 376 #total_row { font-weight: bold; } 377 .passClass { background-color: #6c6; } 378 .failClass { background-color: #c60; } 379 .errorClass { background-color: #c00; } 380 .passCase { color: #6c6; } 381 .failCase { color: #c60; font-weight: bold; } 382 .errorCase { color: #c00; font-weight: bold; } 383 .hiddenRow { display: none; } 384 .testcase { margin-left: 2em; } 385 386 387 /* -- ending ---------------------------------------------------------------------- */ 388 #ending { 389 } 390 391 </style> 392 """ 393 394 395 396 # ------------------------------------------------------------------------ 397 # Heading 398 # 399 400 HEADING_TMPL = """<div class='heading'> 401 <h1>%(title)s</h1> 402 %(parameters)s 403 <p class='description'>%(description)s</p> 404 </div> 405 406 """ # variables: (title, parameters, description) 407 408 HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p> 409 """ # variables: (name, value) 410 411 412 413 # ------------------------------------------------------------------------ 414 # Report 415 # 416 417 REPORT_TMPL = """ 418 <p id='show_detail_line'>Show 419 <a href='javascript:showCase(0)'>Summary</a> 420 <a href='javascript:showCase(1)'>Failed</a> 421 <a href='javascript:showCase(2)'>All</a> 422 </p> 423 <table id='result_table'> 424 <colgroup> 425 <col align='left' /> 426 <col align='right' /> 427 <col align='right' /> 428 <col align='right' /> 429 <col align='right' /> 430 <col align='right' /> 431 </colgroup> 432 <tr id='header_row'> 433 <td>Test Group/Test case</td> 434 <td>Count</td> 435 <td>Pass</td> 436 <td>Fail</td> 437 <td>Error</td> 438 <td>View</td> 439 </tr> 440 %(test_list)s 441 <tr id='total_row'> 442 <td>Total</td> 443 <td>%(count)s</td> 444 <td>%(Pass)s</td> 445 <td>%(fail)s</td> 446 <td>%(error)s</td> 447 <td> </td> 448 </tr> 449 </table> 450 """ # variables: (test_list, count, Pass, fail, error) 451 452 REPORT_CLASS_TMPL = r""" 453 <tr class='%(style)s'> 454 <td>%(desc)s</td> 455 <td>%(count)s</td> 456 <td>%(Pass)s</td> 457 <td>%(fail)s</td> 458 <td>%(error)s</td> 459 <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td> 460 </tr> 461 """ # variables: (style, desc, count, Pass, fail, error, cid) 462 463 464 REPORT_TEST_WITH_OUTPUT_TMPL = r""" 465 <tr id='%(tid)s' class='%(Class)s'> 466 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 467 <td colspan='5' align='center'> 468 469 <!--css div popup start--> 470 <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > 471 %(status)s</a> 472 473 <div id='div_%(tid)s' class="popup_window"> 474 <div style='text-align: right; color:red;cursor:pointer'> 475 <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " > 476 [x]</a> 477 </div> 478 <pre> 479 %(script)s 480 </pre> 481 </div> 482 <!--css div popup end--> 483 484 </td> 485 </tr> 486 """ # variables: (tid, Class, style, desc, status) 487 488 489 REPORT_TEST_NO_OUTPUT_TMPL = r""" 490 <tr id='%(tid)s' class='%(Class)s'> 491 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 492 <td colspan='5' align='center'>%(status)s</td> 493 </tr> 494 """ # variables: (tid, Class, style, desc, status) 495 496 497 REPORT_TEST_OUTPUT_TMPL = r""" 498 %(id)s: %(output)s 499 """ # variables: (id, output) 500 501 502 503 # ------------------------------------------------------------------------ 504 # ENDING 505 # 506 507 ENDING_TMPL = """<div id='ending'> </div>""" 508 509 # -------------------- The end of the Template class ------------------- 510 511 512 TestResult = unittest.TestResult 513 514 class _TestResult(TestResult): 515 # note: _TestResult is a pure representation of results. 516 # It lacks the output and reporting ability compares to unittest._TextTestResult. 517 518 def __init__(self, verbosity=1): 519 TestResult.__init__(self) 520 self.stdout0 = None 521 self.stderr0 = None 522 self.success_count = 0 523 self.failure_count = 0 524 self.error_count = 0 525 self.verbosity = verbosity 526 527 # result is a list of result in 4 tuple 528 # ( 529 # result code (0: success; 1: fail; 2: error), 530 # TestCase object, 531 # Test output (byte string), 532 # stack trace, 533 # ) 534 self.result = [] 535 536 537 def startTest(self, test): 538 TestResult.startTest(self, test) 539 # just one buffer for both stdout and stderr 540 self.outputBuffer = io.StringIO() 541 stdout_redirector.fp = self.outputBuffer 542 stderr_redirector.fp = self.outputBuffer 543 self.stdout0 = sys.stdout 544 self.stderr0 = sys.stderr 545 sys.stdout = stdout_redirector 546 sys.stderr = stderr_redirector 547 548 549 def complete_output(self): 550 """ 551 Disconnect output redirection and return buffer. 552 Safe to call multiple times. 553 """ 554 if self.stdout0: 555 sys.stdout = self.stdout0 556 sys.stderr = self.stderr0 557 self.stdout0 = None 558 self.stderr0 = None 559 return self.outputBuffer.getvalue() 560 561 562 def stopTest(self, test): 563 # Usually one of addSuccess, addError or addFailure would have been called. 564 # But there are some path in unittest that would bypass this. 565 # We must disconnect stdout in stopTest(), which is guaranteed to be called. 566 self.complete_output() 567 568 569 def addSuccess(self, test): 570 self.success_count += 1 571 TestResult.addSuccess(self, test) 572 output = self.complete_output() 573 self.result.append((0, test, output, '')) 574 if self.verbosity > 1: 575 sys.stderr.write('ok ') 576 sys.stderr.write(str(test)) 577 sys.stderr.write('\n') 578 else: 579 sys.stderr.write('.') 580 581 def addError(self, test, err): 582 self.error_count += 1 583 TestResult.addError(self, test, err) 584 _, _exc_str = self.errors[-1] 585 output = self.complete_output() 586 self.result.append((2, test, output, _exc_str)) 587 if self.verbosity > 1: 588 sys.stderr.write('E ') 589 sys.stderr.write(str(test)) 590 sys.stderr.write('\n') 591 else: 592 sys.stderr.write('E') 593 594 def addFailure(self, test, err): 595 self.failure_count += 1 596 TestResult.addFailure(self, test, err) 597 _, _exc_str = self.failures[-1] 598 output = self.complete_output() 599 self.result.append((1, test, output, _exc_str)) 600 if self.verbosity > 1: 601 sys.stderr.write('F ') 602 sys.stderr.write(str(test)) 603 sys.stderr.write('\n') 604 else: 605 sys.stderr.write('F') 606 607 608 class HTMLTestRunner(Template_mixin): 609 """ 610 """ 611 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 612 self.stream = stream 613 self.verbosity = verbosity 614 if title is None: 615 self.title = self.DEFAULT_TITLE 616 else: 617 self.title = title 618 if description is None: 619 self.description = self.DEFAULT_DESCRIPTION 620 else: 621 self.description = description 622 623 self.startTime = datetime.datetime.now() 624 625 626 def run(self, test): 627 "Run the given test case or test suite." 628 result = _TestResult(self.verbosity) 629 test(result) 630 self.stopTime = datetime.datetime.now() 631 self.generateReport(test, result) 632 # print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime) 633 print('\nTime Elapsed: %s' % (self.stopTime - self.startTime), file=sys.stderr) 634 return result 635 636 637 def sortResult(self, result_list): 638 # unittest does not seems to run in any particular order. 639 # Here at least we want to group them together by class. 640 rmap = {} 641 classes = [] 642 for n,t,o,e in result_list: 643 cls = t.__class__ 644 if not cls in rmap: 645 rmap[cls] = [] 646 classes.append(cls) 647 rmap[cls].append((n,t,o,e)) 648 r = [(cls, rmap[cls]) for cls in classes] 649 return r 650 651 652 def getReportAttributes(self, result): 653 """ 654 Return report attributes as a list of (name, value). 655 Override this to add custom attributes. 656 """ 657 startTime = str(self.startTime)[:19] 658 duration = str(self.stopTime - self.startTime) 659 status = [] 660 if result.success_count: status.append('Pass %s' % result.success_count) 661 if result.failure_count: status.append('Failure %s' % result.failure_count) 662 if result.error_count: status.append('Error %s' % result.error_count ) 663 if status: 664 status = ' '.join(status) 665 else: 666 status = 'none' 667 return [ 668 ('Start Time', startTime), 669 ('Duration', duration), 670 ('Status', status), 671 ] 672 673 674 def generateReport(self, test, result): 675 report_attrs = self.getReportAttributes(result) 676 generator = 'HTMLTestRunner %s' % __version__ 677 stylesheet = self._generate_stylesheet() 678 heading = self._generate_heading(report_attrs) 679 report = self._generate_report(result) 680 ending = self._generate_ending() 681 output = self.HTML_TMPL % dict( 682 title = saxutils.escape(self.title), 683 generator = generator, 684 stylesheet = stylesheet, 685 heading = heading, 686 report = report, 687 ending = ending, 688 ) 689 self.stream.write(output.encode('utf8')) 690 691 692 def _generate_stylesheet(self): 693 return self.STYLESHEET_TMPL 694 695 696 def _generate_heading(self, report_attrs): 697 a_lines = [] 698 for name, value in report_attrs: 699 line = self.HEADING_ATTRIBUTE_TMPL % dict( 700 name = saxutils.escape(name), 701 value = saxutils.escape(value), 702 ) 703 a_lines.append(line) 704 heading = self.HEADING_TMPL % dict( 705 title = saxutils.escape(self.title), 706 parameters = ''.join(a_lines), 707 description = saxutils.escape(self.description), 708 ) 709 return heading 710 711 712 def _generate_report(self, result): 713 rows = [] 714 sortedResult = self.sortResult(result.result) 715 for cid, (cls, cls_results) in enumerate(sortedResult): 716 # subtotal for a class 717 np = nf = ne = 0 718 for n,t,o,e in cls_results: 719 if n == 0: np += 1 720 elif n == 1: nf += 1 721 else: ne += 1 722 723 # format class description 724 if cls.__module__ == "__main__": 725 name = cls.__name__ 726 else: 727 name = "%s.%s" % (cls.__module__, cls.__name__) 728 doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 729 desc = doc and '%s: %s' % (name, doc) or name 730 731 row = self.REPORT_CLASS_TMPL % dict( 732 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 733 desc = desc, 734 count = np+nf+ne, 735 Pass = np, 736 fail = nf, 737 error = ne, 738 cid = 'c%s' % (cid+1), 739 ) 740 rows.append(row) 741 742 for tid, (n,t,o,e) in enumerate(cls_results): 743 self._generate_report_test(rows, cid, tid, n, t, o, e) 744 745 report = self.REPORT_TMPL % dict( 746 test_list = ''.join(rows), 747 count = str(result.success_count+result.failure_count+result.error_count), 748 Pass = str(result.success_count), 749 fail = str(result.failure_count), 750 error = str(result.error_count), 751 ) 752 return report 753 754 755 def _generate_report_test(self, rows, cid, tid, n, t, o, e): 756 # e.g. 'pt1.1', 'ft1.1', etc 757 has_output = bool(o or e) 758 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 759 name = t.id().split('.')[-1] 760 doc = t.shortDescription() or "" 761 desc = doc and ('%s: %s' % (name, doc)) or name 762 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 763 764 # o and e should be byte string because they are collected from stdout and stderr? 765 if isinstance(o,str): 766 # TODO: some problem with 'string_escape': it escape \n and mess up formating 767 # uo = unicode(o.encode('string_escape')) 768 uo = o 769 else: 770 uo = o.decode('utf-8') 771 if isinstance(e,str): 772 # TODO: some problem with 'string_escape': it escape \n and mess up formating 773 # ue = unicode(e.encode('string_escape')) 774 ue = e 775 else: 776 ue = e.decode('utf-8') 777 778 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 779 id = tid, 780 output = saxutils.escape(uo+ue), 781 ) 782 783 row = tmpl % dict( 784 tid = tid, 785 Class = (n == 0 and 'hiddenRow' or 'none'), 786 style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), 787 desc = desc, 788 script = script, 789 status = self.STATUS[n], 790 ) 791 rows.append(row) 792 if not has_output: 793 return 794 795 def _generate_ending(self): 796 return self.ENDING_TMPL 797 798 799 ############################################################################## 800 # Facilities for running tests from the command line 801 ############################################################################## 802 803 # Note: Reuse unittest.TestProgram to launch test. In the future we may 804 # build our own launcher to support more specific command line 805 # parameters like test title, CSS, etc. 806 class TestProgram(unittest.TestProgram): 807 """ 808 A variation of the unittest.TestProgram. Please refer to the base 809 class for command line parameters. 810 """ 811 def runTests(self): 812 # Pick HTMLTestRunner as the default test runner. 813 # base class's testRunner parameter is not useful because it means 814 # we have to instantiate HTMLTestRunner before we know self.verbosity. 815 if self.testRunner is None: 816 self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 817 unittest.TestProgram.runTests(self) 818 819 main = TestProgram 820 821 ############################################################################## 822 # Executing this module from the command line 823 ############################################################################## 824 825 if __name__ == "__main__": 826 main(module=None)
生成報告效果
test_aaa.py:具體代碼如下