13、Unittest单元测试框架使用教程
Unittest单元测试框架使用教程
unittest单元测试框架简介:
是由python提供的单元测试框架,可以帮助我们完成接口自动化和UI自动化测试,主要使用来组织和执行测试用例,它可以将测试用例组织到一起执行,如果某条测试用例执行失败,其他的测试用例能够继续执行,unittest框架还提供了丰富的断言方法,用来判断测试是否通过,最终生成测试结果报告
Unittest框架的四个核心概念:
TestCase:用户自定义的测试用例基类,调用run()方法时,会一次调用setUp()方法、执行用例方法、tearDown()方法
TestSuite:测试用例集合,可以通过addTest()方法手动添加TestCase,还能够通过TestLoader自动添加TestCase,TestLoader自动添加时没有顺序
TestRunner:运行测试用例的驱动类,可以执行TestCase,也可以执行TestSuite,执行后TestCase和TestSuite会自动管理TestResult
TestFixture:进行测试活动的准备工作,比如创建临时数据库、文件、目录等,其中setUP()方法和setDown()方法是最常用的
Unittest测试流程
- 写好一个TestCase:定义一个class类,继承unittest.TestCase,就是一条测试用例,类中定义以test开头的方法,每个test开头的方法在load到suit中时都会生成一条测试用例
- 由TestLoader加载TestCase到TestSuite中
- TestRunner运行TestSuite,运行结果保存在TextTestResult中
TestCase使用:
# test_001.py import unittest def add(x, y): return x + y class Test_Case_01(unitest.TestCase): # 新建测试类必须继承unittest.TestCase # 测试方法名称必须以 test 开头 def test_add_01(self): result = add(2, 4) print(f'两数和为:{result}') def test_add_02(self): result = add(5, 7) print(f'两数之和为:{result}') if __name__ == '__main__': # 执行测试用例 unittest.main()
TestSuite使用:
- 实例化:suite = unittest.TestSuite()
- 添加用例:suite.addTest(ClassName(“MethodName”)),ClassName:类名,MethodName:方法名)
- 添加扩展:suite.addTest(unittest.makeSuite(ClassName)),搜索指定ClassName内 test 开头的方法,并添加到测试套件中
- TestSuite需要配合TestRunner才能被执行
# 案例 import unittest from test_001 import Test_Case_01 # 实例化测试套件对象 suite = unittest.TestSuite() # 单独添加测试用例 suite.addTest(Test_Case_01("test_add_01")) suite.addTest(Test_Case_01("test_add_02")) # 批量添加用例, addTest()添加的次数根据Test_Case_01中的测试用例个数决定,这里是添加2次 suite.addTest(unittest.makeSuite(Test_Case_01)) # 执行 runner = unittest.TextTestRunner() runner.run(suite)
TextTestRunner使用
- 实例化:runner = unittest.TextTestRunner()
- 执行:runner.run(suite) ,suite为测试套件的名称,代码示例参考TestSuit使用代码即可
runner = unittest.TextTestRunner()
runner.run(suite)
TestLoader的使用
-
用来加载TestCase到TestSuite中,即加载满足条件的测试用例,并把测试用例封装成测试套件。
- 使用unittest.TestLoader(),通过该类下面的discover()方法自动搜索指定目录下指定开头的 .py 文件,并将查到的测试用例组装到测试套件。
# 示例 import os import unittest # 获取根目录 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 获取文件的根目录 # 测试用例地址 test_case_path = os.path.join(BASE_DIR, "Scripts") suit = unittest.TestSuite() loader = unittest.TestLoader() # 专门用来查找用例的实例 # test_case_path:测试用例的所在路径。pattern:搜索以test开头的py文件,默认为‘test*.py’ suit.addTests(loader.discover(test_case_path, pattern="test*.py")) runner = unittest.TextTestRunner() runner.run(suit)
注意: 也可以使用unittest.defaultTestLoader 代替 unittest.TestLoader()
discover = unittest.defaultTestLoader.discover(test_case_path, pattern="t*.py") suit.addTests(discover)
TestSuite和TestLoader的区别
- 共同点:都是测试套件
- 不同点:实现方式不同
- TestSuite需要手动添加测试用例(可以添加测试类,也可以添加测试类中的某个测试方法)
- TestLoader搜索指定目录下指定开头的py文件,并添加测试类中的所有测试方法,不能指定添加某个测试方法
断言
- 比较预期结果和实际结果是否一致,从而判断测试用例是否执行通过
- 断言的方法在unittest.testCase类中已经定义好了,我们的自定义测试类继承了unittest.TestCase类,所以可以直接使用
- 断言方法如下
-
assertEqual(a, b, msg='测试失败时打印的信息') # a == b 断言a和b是否相等,相等则测试用例通过 assertNotEqual(a, b, msg='测试失败时打印的信息') # a != b 断言a和b是否相等,不相等则测试用例通过 assertTrue(x, msg='测试失败时打印的信息') # x is True 断言x是否True,是True则测试用例通过 assertFalse(x, msg='测试失败时打印的信息') # x is False 断言x是否False,是False则测试用例通过 assertIn(a, b, msg='测试失败时打印的信息') # a in b 断言a是否在b中,在b中则测试用例通过 assertNotIn(a, b, msg='测试失败时打印的信息') # a not in b 断言a是否在b中,不在b中则测试用例通过 assertIsNone(x,msg='测试失败时打印的信息') # x is None 断言x是否None,是None则测试用例通过 assertIsNotNone(x, msg='测试失败时打印的信息') # x not is None 断言x是否None,不是None则测试用例通过 assertIs(a, b, msg='测试失败时打印的信息') # a is b 断言a是否是b,是则测试用例通过 assertNotIs(a, b, msg='测试失败时打印的信息') # a not is b 断言a是否是b,不是则测试用例通过 assertIsInstance(a, b, msg='测试失败时打印的信息') # 断言a是是b的一个实例,是则测试用例通过 assertNotIsInstance(a, b, msg='测试失败时打印的信息') # 断言a是是b的一个实例,不是则测试用例通过
- 代码示例:比较字符串是否相同
import unittest from time import sleep from selenium import webdriver from selenium.webdriver.support.ui import Select from selenium.webdriver.common.action_chains import ActionChains class XiCheng(unittest.TestCase): def setUp(self): # 获取谷歌浏览器驱动 self.driver = webdriver.Chrome() # 打开携程登录页面 self.driver.get( 'https://passport.ctrip.com/user/login?BackUrl=https%3A%2F%2Fwww.ctrip.com%2F#ctm_ref=c_ph_login_buttom') # 浏览器最大化 self.driver.maximize_window() # 显示等待 self.driver.implicitly_wait(10) def tearDown(self): sleep(5) self.driver.quit() def test_xiecheng_login(self): # 定位用户名输入框,并输入用户名 self.driver.find_element_by_id('nloginname').send_keys('17696295304') sleep(1) # 输入密码 self.driver.find_element_by_xpath("//*[@id='npwd']").send_keys('hyf19940513') sleep(1) # 勾选协议 xieyi = self.driver.find_element_by_xpath('//*[@id="checkboxAgreementInput"]') self.driver.execute_script("arguments[0].click();", xieyi) # 点击登录按钮 self.driver.find_element_by_id('nsubmit').click() sleep(10) # 获取页面上显示的用户名 username = self.driver.find_element_by_xpath('//*[@id="hp_nfes_accountbar"]/li[1]/div/button/div/span').text # 断言:比较页面读取到的用户名和真实的用户名是否一致 self.assertEqual(username,'东街刘华强', msg = '用户名不正确,页面显示用户名为'+username) if __name__ == '__main__': unittest.main()
HTMLTestRunner生成html测试报告
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 The simplest way to use this is to invoke its main method. E.g. 5 import unittest 6 import HTMLTestRunner 7 ... define your tests ... 8 if __name__ == '__main__': 9 HTMLTestRunner.main() 10 For more customization options, instantiates a HTMLTestRunner object. 11 HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. 12 # output to a file 13 fp = file('my_report.html', 'wb') 14 runner = HTMLTestRunner.HTMLTestRunner( 15 stream=fp, 16 title='My unit test', 17 description='This demonstrates the report output by HTMLTestRunner.' 18 ) 19 # Use an external stylesheet. 20 # See the Template_mixin class for more customizable options 21 runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' 22 # run the test 23 runner.run(my_test_suite) 24 ------------------------------------------------------------------------ 25 Copyright (c) 2004-2007, Wai Yip Tung 26 All rights reserved. 27 Redistribution and use in source and binary forms, with or without 28 modification, are permitted provided that the following conditions are 29 met: 30 * Redistributions of source code must retain the above copyright notice, 31 this list of conditions and the following disclaimer. 32 * Redistributions in binary form must reproduce the above copyright 33 notice, this list of conditions and the following disclaimer in the 34 documentation and/or other materials provided with the distribution. 35 * Neither the name Wai Yip Tung nor the names of its contributors may be 36 used to endorse or promote products derived from this software without 37 specific prior written permission. 38 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 39 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 40 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 41 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 42 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 43 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 44 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 45 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 46 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 47 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 48 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 49 """ 50 51 # URL: http://tungwaiyip.info/software/HTMLTestRunner.html 52 53 __author__ = "Wai Yip Tung" 54 __version__ = "0.8.2" 55 56 57 """ 58 Change History 59 Version 0.8.2 60 * Show output inline instead of popup window (Viorel Lupu). 61 Version in 0.8.1 62 * Validated XHTML (Wolfgang Borgert). 63 * Added description of test classes and test cases. 64 Version in 0.8.0 65 * Define Template_mixin class for customization. 66 * Workaround a IE 6 bug that it does not treat <script> block as CDATA. 67 Version in 0.7.1 68 * Back port to Python 2.3 (Frank Horowitz). 69 * Fix missing scroll bars in detail log (Podi). 70 """ 71 72 # TODO: color stderr 73 # TODO: simplify javascript using ,ore than 1 class in the class attribute? 74 75 import datetime 76 import io 77 import sys 78 import time 79 import unittest 80 from xml.sax import saxutils 81 82 83 # ------------------------------------------------------------------------ 84 # The redirectors below are used to capture output during testing. Output 85 # sent to sys.stdout and sys.stderr are automatically captured. However 86 # in some cases sys.stdout is already cached before HTMLTestRunner is 87 # invoked (e.g. calling logging.basicConfig). In order to capture those 88 # output, use the redirectors for the cached stream. 89 # 90 # e.g. 91 # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) 92 # >>> 93 94 class OutputRedirector(object): 95 """ Wrapper to redirect stdout or stderr """ 96 def __init__(self, fp): 97 self.fp = fp 98 99 def write(self, s): 100 self.fp.write(bytes(s,'UTF-8')) 101 102 def writelines(self, lines): 103 self.fp.writelines(lines) 104 105 def flush(self): 106 self.fp.flush() 107 108 stdout_redirector = OutputRedirector(sys.stdout) 109 stderr_redirector = OutputRedirector(sys.stderr) 110 111 112 113 # ---------------------------------------------------------------------- 114 # Template 115 116 class Template_mixin(object): 117 """ 118 Define a HTML template for report customerization and generation. 119 Overall structure of an HTML report 120 HTML 121 +------------------------+ 122 |<html> | 123 | <head> | 124 | | 125 | STYLESHEET | 126 | +----------------+ | 127 | | | | 128 | +----------------+ | 129 | | 130 | </head> | 131 | | 132 | <body> | 133 | | 134 | HEADING | 135 | +----------------+ | 136 | | | | 137 | +----------------+ | 138 | | 139 | REPORT | 140 | +----------------+ | 141 | | | | 142 | +----------------+ | 143 | | 144 | ENDING | 145 | +----------------+ | 146 | | | | 147 | +----------------+ | 148 | | 149 | </body> | 150 |</html> | 151 +------------------------+ 152 """ 153 154 STATUS = { 155 0: 'pass', 156 1: 'fail', 157 2: 'error', 158 } 159 160 DEFAULT_TITLE = 'Unit Test Report' 161 DEFAULT_DESCRIPTION = '' 162 163 # ------------------------------------------------------------------------ 164 # HTML Template 165 166 HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> 167 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 168 <html xmlns="http://www.w3.org/1999/xhtml"> 169 <head> 170 <title>%(title)s</title> 171 <meta name="generator" content="%(generator)s"/> 172 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 173 %(stylesheet)s 174 </head> 175 <body> 176 <script language="javascript" type="text/javascript"><!-- 177 output_list = Array(); 178 /* level - 0:Summary; 1:Failed; 2:All */ 179 function showCase(level) { 180 trs = document.getElementsByTagName("tr"); 181 for (var i = 0; i < trs.length; i++) { 182 tr = trs[i]; 183 id = tr.id; 184 if (id.substr(0,2) == 'ft') { 185 if (level < 1) { 186 tr.className = 'hiddenRow'; 187 } 188 else { 189 tr.className = ''; 190 } 191 } 192 if (id.substr(0,2) == 'pt') { 193 if (level > 1) { 194 tr.className = ''; 195 } 196 else { 197 tr.className = 'hiddenRow'; 198 } 199 } 200 } 201 } 202 function showClassDetail(cid, count) { 203 var id_list = Array(count); 204 var toHide = 1; 205 for (var i = 0; i < count; i++) { 206 tid0 = 't' + cid.substr(1) + '.' + (i+1); 207 tid = 'f' + tid0; 208 tr = document.getElementById(tid); 209 if (!tr) { 210 tid = 'p' + tid0; 211 tr = document.getElementById(tid); 212 } 213 id_list[i] = tid; 214 if (tr.className) { 215 toHide = 0; 216 } 217 } 218 for (var i = 0; i < count; i++) { 219 tid = id_list[i]; 220 if (toHide) { 221 document.getElementById('div_'+tid).style.display = 'none' 222 document.getElementById(tid).className = 'hiddenRow'; 223 } 224 else { 225 document.getElementById(tid).className = ''; 226 } 227 } 228 } 229 function showTestDetail(div_id){ 230 var details_div = document.getElementById(div_id) 231 var displayState = details_div.style.display 232 // alert(displayState) 233 if (displayState != 'block' ) { 234 displayState = 'block' 235 details_div.style.display = 'block' 236 } 237 else { 238 details_div.style.display = 'none' 239 } 240 } 241 function html_escape(s) { 242 s = s.replace(/&/g,'&'); 243 s = s.replace(/</g,'<'); 244 s = s.replace(/>/g,'>'); 245 return s; 246 } 247 /* obsoleted by detail in <div> 248 function showOutput(id, name) { 249 var w = window.open("", //url 250 name, 251 "resizable,scrollbars,status,width=800,height=450"); 252 d = w.document; 253 d.write("<pre>"); 254 d.write(html_escape(output_list[id])); 255 d.write("\n"); 256 d.write("<a href='javascript:window.close()'>close</a>\n"); 257 d.write("</pre>\n"); 258 d.close(); 259 } 260 */ 261 --></script> 262 %(heading)s 263 %(report)s 264 %(ending)s 265 </body> 266 </html> 267 """ 268 # variables: (title, generator, stylesheet, heading, report, ending) 269 270 271 # ------------------------------------------------------------------------ 272 # Stylesheet 273 # 274 # alternatively use a <link> for external style sheet, e.g. 275 # <link rel="stylesheet" href="$url" type="text/css"> 276 277 STYLESHEET_TMPL = """ 278 <style type="text/css" media="screen"> 279 body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; } 280 table { font-size: 100%; } 281 pre { } 282 /* -- heading ---------------------------------------------------------------------- */ 283 h1 { 284 font-size: 16pt; 285 color: gray; 286 } 287 .heading { 288 margin-top: 0ex; 289 margin-bottom: 1ex; 290 } 291 .heading .attribute { 292 margin-top: 1ex; 293 margin-bottom: 0; 294 } 295 .heading .description { 296 margin-top: 4ex; 297 margin-bottom: 6ex; 298 } 299 /* -- css div popup ------------------------------------------------------------------------ */ 300 a.popup_link { 301 } 302 a.popup_link:hover { 303 color: red; 304 } 305 .popup_window { 306 display: none; 307 position: relative; 308 left: 0px; 309 top: 0px; 310 /*border: solid #627173 1px; */ 311 padding: 10px; 312 background-color: #E6E6D6; 313 font-family: "Lucida Console", "Courier New", Courier, monospace; 314 text-align: left; 315 font-size: 8pt; 316 width: 500px; 317 } 318 } 319 /* -- report ------------------------------------------------------------------------ */ 320 #show_detail_line { 321 margin-top: 3ex; 322 margin-bottom: 1ex; 323 } 324 #result_table { 325 width: 80%; 326 border-collapse: collapse; 327 border: 1px solid #777; 328 } 329 #header_row { 330 font-weight: bold; 331 color: white; 332 background-color: #777; 333 } 334 #result_table td { 335 border: 1px solid #777; 336 padding: 2px; 337 } 338 #total_row { font-weight: bold; } 339 .passClass { background-color: #6c6; } 340 .failClass { background-color: #c60; } 341 .errorClass { background-color: #c00; } 342 .passCase { color: #6c6; } 343 .failCase { color: #c60; font-weight: bold; } 344 .errorCase { color: #c00; font-weight: bold; } 345 .hiddenRow { display: none; } 346 .testcase { margin-left: 2em; } 347 /* -- ending ---------------------------------------------------------------------- */ 348 #ending { 349 } 350 </style> 351 """ 352 353 354 355 # ------------------------------------------------------------------------ 356 # Heading 357 # 358 359 HEADING_TMPL = """<div class='heading'> 360 <h1>%(title)s</h1> 361 %(parameters)s 362 <p class='description'>%(description)s</p> 363 </div> 364 """ # variables: (title, parameters, description) 365 366 HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p> 367 """ # variables: (name, value) 368 369 370 371 # ------------------------------------------------------------------------ 372 # Report 373 # 374 375 REPORT_TMPL = """ 376 <p id='show_detail_line'>Show 377 <a href='javascript:showCase(0)'>Summary</a> 378 <a href='javascript:showCase(1)'>Failed</a> 379 <a href='javascript:showCase(2)'>All</a> 380 </p> 381 <table id='result_table'> 382 <colgroup> 383 <col align='left' /> 384 <col align='right' /> 385 <col align='right' /> 386 <col align='right' /> 387 <col align='right' /> 388 <col align='right' /> 389 </colgroup> 390 <tr id='header_row'> 391 <td>Test Group/Test case</td> 392 <td>Count</td> 393 <td>Pass</td> 394 <td>Fail</td> 395 <td>Error</td> 396 <td>View</td> 397 </tr> 398 %(test_list)s 399 <tr id='total_row'> 400 <td>Total</td> 401 <td>%(count)s</td> 402 <td>%(Pass)s</td> 403 <td>%(fail)s</td> 404 <td>%(error)s</td> 405 <td> </td> 406 </tr> 407 </table> 408 """ # variables: (test_list, count, Pass, fail, error) 409 410 REPORT_CLASS_TMPL = r""" 411 <tr class='%(style)s'> 412 <td>%(desc)s</td> 413 <td>%(count)s</td> 414 <td>%(Pass)s</td> 415 <td>%(fail)s</td> 416 <td>%(error)s</td> 417 <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td> 418 </tr> 419 """ # variables: (style, desc, count, Pass, fail, error, cid) 420 421 422 REPORT_TEST_WITH_OUTPUT_TMPL = r""" 423 <tr id='%(tid)s' class='%(Class)s'> 424 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 425 <td colspan='5' align='center'> 426 <!--css div popup start--> 427 <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > 428 %(status)s</a> 429 <div id='div_%(tid)s' class="popup_window"> 430 <div style='text-align: right; color:red;cursor:pointer'> 431 <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " > 432 [x]</a> 433 </div> 434 <pre> 435 %(script)s 436 </pre> 437 </div> 438 <!--css div popup end--> 439 </td> 440 </tr> 441 """ # variables: (tid, Class, style, desc, status) 442 443 444 REPORT_TEST_NO_OUTPUT_TMPL = r""" 445 <tr id='%(tid)s' class='%(Class)s'> 446 <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> 447 <td colspan='5' align='center'>%(status)s</td> 448 </tr> 449 """ # variables: (tid, Class, style, desc, status) 450 451 452 REPORT_TEST_OUTPUT_TMPL = r""" 453 %(id)s: %(output)s 454 """ # variables: (id, output) 455 456 457 458 # ------------------------------------------------------------------------ 459 # ENDING 460 # 461 462 ENDING_TMPL = """<div id='ending'> </div>""" 463 464 # -------------------- The end of the Template class ------------------- 465 466 467 TestResult = unittest.TestResult 468 469 class _TestResult(TestResult): 470 # note: _TestResult is a pure representation of results. 471 # It lacks the output and reporting ability compares to unittest._TextTestResult. 472 473 def __init__(self, verbosity=1): 474 TestResult.__init__(self) 475 self.stdout0 = None 476 self.stderr0 = None 477 self.success_count = 0 478 self.failure_count = 0 479 self.error_count = 0 480 self.verbosity = verbosity 481 482 # result is a list of result in 4 tuple 483 # ( 484 # result code (0: success; 1: fail; 2: error), 485 # TestCase object, 486 # Test output (byte string), 487 # stack trace, 488 # ) 489 self.result = [] 490 491 492 def startTest(self, test): 493 TestResult.startTest(self, test) 494 # just one buffer for both stdout and stderr 495 self.outputBuffer = io.BytesIO() 496 stdout_redirector.fp = self.outputBuffer 497 stderr_redirector.fp = self.outputBuffer 498 self.stdout0 = sys.stdout 499 self.stderr0 = sys.stderr 500 sys.stdout = stdout_redirector 501 sys.stderr = stderr_redirector 502 503 504 def complete_output(self): 505 """ 506 Disconnect output redirection and return buffer. 507 Safe to call multiple times. 508 """ 509 if self.stdout0: 510 sys.stdout = self.stdout0 511 sys.stderr = self.stderr0 512 self.stdout0 = None 513 self.stderr0 = None 514 return self.outputBuffer.getvalue() 515 516 517 def stopTest(self, test): 518 # Usually one of addSuccess, addError or addFailure would have been called. 519 # But there are some path in unittest that would bypass this. 520 # We must disconnect stdout in stopTest(), which is guaranteed to be called. 521 self.complete_output() 522 523 524 def addSuccess(self, test): 525 self.success_count += 1 526 TestResult.addSuccess(self, test) 527 output = self.complete_output() 528 self.result.append((0, test, output, '')) 529 if self.verbosity > 1: 530 sys.stderr.write('ok ') 531 sys.stderr.write(str(test)) 532 sys.stderr.write('\n') 533 else: 534 sys.stderr.write('.') 535 536 def addError(self, test, err): 537 self.error_count += 1 538 TestResult.addError(self, test, err) 539 _, _exc_str = self.errors[-1] 540 output = self.complete_output() 541 self.result.append((2, test, output, _exc_str)) 542 if self.verbosity > 1: 543 sys.stderr.write('E ') 544 sys.stderr.write(str(test)) 545 sys.stderr.write('\n') 546 else: 547 sys.stderr.write('E') 548 549 def addFailure(self, test, err): 550 self.failure_count += 1 551 TestResult.addFailure(self, test, err) 552 _, _exc_str = self.failures[-1] 553 output = self.complete_output() 554 self.result.append((1, test, output, _exc_str)) 555 if self.verbosity > 1: 556 sys.stderr.write('F ') 557 sys.stderr.write(str(test)) 558 sys.stderr.write('\n') 559 else: 560 sys.stderr.write('F') 561 562 563 class HTMLTestRunner(Template_mixin): 564 """ 565 """ 566 def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 567 self.stream = stream 568 self.verbosity = verbosity 569 if title is None: 570 self.title = self.DEFAULT_TITLE 571 else: 572 self.title = title 573 if description is None: 574 self.description = self.DEFAULT_DESCRIPTION 575 else: 576 self.description = description 577 578 self.startTime = datetime.datetime.now() 579 580 581 def run(self, test): 582 "Run the given test case or test suite." 583 result = _TestResult(self.verbosity) 584 test(result) 585 self.stopTime = datetime.datetime.now() 586 self.generateReport(test, result) 587 print('\nTime Elapsed: %s' % (self.stopTime-self.startTime),file=sys.stderr) 588 return result 589 590 591 def sortResult(self, result_list): 592 # unittest does not seems to run in any particular order. 593 # Here at least we want to group them together by class. 594 rmap = {} 595 classes = [] 596 for n,t,o,e in result_list: 597 cls = t.__class__ 598 if not cls in rmap: 599 rmap[cls] = [] 600 classes.append(cls) 601 rmap[cls].append((n,t,o,e)) 602 r = [(cls, rmap[cls]) for cls in classes] 603 return r 604 605 606 def getReportAttributes(self, result): 607 """ 608 Return report attributes as a list of (name, value). 609 Override this to add custom attributes. 610 """ 611 startTime = str(self.startTime)[:19] 612 duration = str(self.stopTime - self.startTime) 613 status = [] 614 if result.success_count: status.append('Pass %s' % result.success_count) 615 if result.failure_count: status.append('Failure %s' % result.failure_count) 616 if result.error_count: status.append('Error %s' % result.error_count ) 617 if status: 618 status = ' '.join(status) 619 else: 620 status = 'none' 621 return [ 622 ('Start Time', startTime), 623 ('Duration', duration), 624 ('Status', status), 625 ] 626 627 628 def generateReport(self, test, result): 629 report_attrs = self.getReportAttributes(result) 630 generator = 'HTMLTestRunner %s' % __version__ 631 stylesheet = self._generate_stylesheet() 632 heading = self._generate_heading(report_attrs) 633 report = self._generate_report(result) 634 ending = self._generate_ending() 635 output = self.HTML_TMPL % dict( 636 title = saxutils.escape(self.title), 637 generator = generator, 638 stylesheet = stylesheet, 639 heading = heading, 640 report = report, 641 ending = ending, 642 ) 643 self.stream.write(output.encode('utf8')) 644 645 646 def _generate_stylesheet(self): 647 return self.STYLESHEET_TMPL 648 649 650 def _generate_heading(self, report_attrs): 651 a_lines = [] 652 for name, value in report_attrs: 653 line = self.HEADING_ATTRIBUTE_TMPL % dict( 654 name = saxutils.escape(name), 655 value = saxutils.escape(value), 656 ) 657 a_lines.append(line) 658 heading = self.HEADING_TMPL % dict( 659 title = saxutils.escape(self.title), 660 parameters = ''.join(a_lines), 661 description = saxutils.escape(self.description), 662 ) 663 return heading 664 665 666 def _generate_report(self, result): 667 rows = [] 668 sortedResult = self.sortResult(result.result) 669 for cid, (cls, cls_results) in enumerate(sortedResult): 670 # subtotal for a class 671 np = nf = ne = 0 672 for n,t,o,e in cls_results: 673 if n == 0: np += 1 674 elif n == 1: nf += 1 675 else: ne += 1 676 677 # format class description 678 if cls.__module__ == "__main__": 679 name = cls.__name__ 680 else: 681 name = "%s.%s" % (cls.__module__, cls.__name__) 682 doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 683 desc = doc and '%s: %s' % (name, doc) or name 684 685 row = self.REPORT_CLASS_TMPL % dict( 686 style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 687 desc = desc, 688 count = np+nf+ne, 689 Pass = np, 690 fail = nf, 691 error = ne, 692 cid = 'c%s' % (cid+1), 693 ) 694 rows.append(row) 695 696 for tid, (n,t,o,e) in enumerate(cls_results): 697 self._generate_report_test(rows, cid, tid, n, t, o, e) 698 699 report = self.REPORT_TMPL % dict( 700 test_list = ''.join(rows), 701 count = str(result.success_count+result.failure_count+result.error_count), 702 Pass = str(result.success_count), 703 fail = str(result.failure_count), 704 error = str(result.error_count), 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 tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) 713 name = t.id().split('.')[-1] 714 doc = t.shortDescription() or "" 715 desc = doc and ('%s: %s' % (name, doc)) or name 716 tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL 717 718 # o and e should be byte string because they are collected from stdout and stderr? 719 if isinstance(o,str): 720 # TODO: some problem with 'string_escape': it escape \n and mess up formating 721 # uo = unicode(o.encode('string_escape')) 722 uo = o 723 else: 724 uo = o.decode('utf-8') 725 if isinstance(e,str): 726 # TODO: some problem with 'string_escape': it escape \n and mess up formating 727 # ue = unicode(e.encode('string_escape')) 728 ue = e 729 else: 730 ue = e.decode('utf-8') 731 732 script = self.REPORT_TEST_OUTPUT_TMPL % dict( 733 id = tid, 734 output = saxutils.escape(uo+ue), 735 ) 736 737 row = tmpl % dict( 738 tid = tid, 739 Class = (n == 0 and 'hiddenRow' or 'none'), 740 style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), 741 desc = desc, 742 script = script, 743 status = self.STATUS[n], 744 ) 745 rows.append(row) 746 if not has_output: 747 return 748 749 def _generate_ending(self): 750 return self.ENDING_TMPL 751 752 753 ############################################################################## 754 # Facilities for running tests from the command line 755 ############################################################################## 756 757 # Note: Reuse unittest.TestProgram to launch test. In the future we may 758 # build our own launcher to support more specific command line 759 # parameters like test title, CSS, etc. 760 class TestProgram(unittest.TestProgram): 761 """ 762 A variation of the unittest.TestProgram. Please refer to the base 763 class for command line parameters. 764 """ 765 def runTests(self): 766 # Pick HTMLTestRunner as the default test runner. 767 # base class's testRunner parameter is not useful because it means 768 # we have to instantiate HTMLTestRunner before we know self.verbosity. 769 if self.testRunner is None: 770 self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 771 unittest.TestProgram.runTests(self) 772 773 main = TestProgram 774 775 ############################################################################## 776 # Executing this module from the command line 777 ############################################################################## 778 779 if __name__ == "__main__": 780 main(module=None)
- 创建一个文件,名称“HTMLTestRunner.py”,放在python/Lib目录下,将上述代码复制到文件中保存
- 需要导包:from HTMLTestRunner import HTMLTestRunner
- 代码示例
测试用例代码
from selenium import webdriver from time import sleep import unittest class SwagLabs(unittest.TestCase): def setUp(self) -> None: # 获取浏览器驱动、 self.driver = webdriver.Chrome() # 打开测试网页 self.driver.get('https://www.saucedemo.com/') # 浏览器最大化 self.driver.maximize_window() # 隐式等待页面加载完成 self.driver.implicitly_wait(10) def tearDown(self) -> None: # 等待5s,退出浏览器 sleep(5) self.driver.quit() def test_login(self): # 用户名输入框输入“standard_user” self.driver.find_element_by_id('user-name').send_keys('standard_user') # 密码输入框输入“standard_user” self.driver.find_element_by_id('password').send_keys('secret_sauce') # 点击登陆按钮 self.driver.find_element_by_id('login-button').click() # 定位页面标题,对比是否是Swag Labs来判断是否登陆成功 text_title = self.driver.find_element_by_xpath('//*[@id="header_container"]/div[1]/div[2]/div').text self.assertEqual(text_title, 'Swag Labs', msg='登陆失败,未获取指定字符串Swag Labs') if __name__ == '__main__': unittest.main()
执行代码
1 import unittest 2 from HTMLTestRunner import HTMLTestRunner 3 import time 4 from open_chrome import SwagLabs 5 import os 6 7 # 定义报告存放目录 8 report_path = './reports/' 9 10 # 组装报告文件路径:/reports/2023-04-02 12:00:00.html 11 now_time_str = time.strftime('%Y-%m-%d', time.localtime()) + '.html' 12 print(now_time_str) 13 file_path = os.path.join(report_path,now_time_str) 14 print(file_path) 15 16 # 打开报告存放文件 17 with open(file_path, mode='wb') as fl: 18 # 创建测试集合 19 suit = unittest.TestSuite() 20 21 # 将测试用例添加到测试集合 22 suit.addTest(SwagLabs('test_login')) 23 # 将测试用例添加到测试集合方式二 24 # suit.addTest(unittest.makeSuite(SwagLabs)) 25 26 # HTMLTestRunner类启动测试运行测试集合 27 runner = HTMLTestRunner(title='测试标题', description='描述本次测试的大概内容', stream=fl) 28 runner.run(suit)
浙公网安备 33010602011771号