unittest 是python 语言的单元测试框架,提供了创建测试用例、测试套件和批量执行测试用例的方案。
利用单元测试框架,可以创建一个类,该类继承 unttest 的 TestCase ,这样可以把每个 TestCase 看成是一个最小的单位,由测试套件组织起来,运行时直接执行即可,同时可以引入测试报告。
一、测试固件
测试固件用于处理初始化操作,可以使测试固件只执行一次。
1、测试固件每次均执行
unittest 单元测试框架中,提供了setUp 和 tesrDown 的测试固件。
import unittest class BaiduTest(unittest.TestCase): def setUp(self): #N测试点打开N次 print('start') def tearDown(self): print('end') def test_baidu_so(self): print('测试用例执行') def test_baidu_hi(self): print('hi') if __name__ == '__name__': unittest.main(verbosity=2)
执行顺序是先执行 setUp 方法,再执行 test_baidu_so 或者 test_baidu_hi 其中一个,最后执行 tearDown 方法。有N个用例,setUp 方法、tearDown 方法 都会执行N次。
2、测试固件只执行一次
使用 setUpClass 和 tearDownClass 方法。
import unittest class BaiduTest(unittest.TestCase): @classmethod #该测试固件方法是类测试方法,需要在方法上面加上装饰器 def setUpClass(cls): #多次测试点只打开一次 print('start') @classmethod def tearDownClass(cls): print('end') def test_baidu_so(self): print('测试用例执行') def test_baidu_hi(self): print('hi') if __name__ == '__name__': unittest.main(verbosity=2)
有N个用例,setUpClass 和 tearDownClass 方法 只会执行1次。
但有一个问题,如:以百度首页测试点击新闻页面和测试点击地图页面为例,意味着点击新闻页面后,需要回到百度首页后才可以找得到地图页面的链接进行点击。
所以,在实际的工作中,尽量使用测试固件 setUp 和 tearDown ,使得自动化测试用例之间没有关联性,避免一个测试用例执行失败是由于上一个测试用例导致的。
二、测试执行
测试用例的执行是在主函数中,unittest 调用的是 main,代码如下:
main = TestProgram
TestProgram 还是一个类,该类的构造函数如下代码:
class TestProgram(object): """A command-line program that runs a set of tests; this is primarily for making test modules conveniently executable. """ # defaults for testing module=None verbosity = 1 failfast = catchbreak = buffer = progName = warnings = testNamePatterns = None _discovery_parser = None def __init__(self, module='__main__', defaultTest=None, argv=None, testRunner=None, testLoader=loader.defaultTestLoader, exit=True, verbosity=1, failfast=None, catchbreak=None, buffer=None, warnings=None, *, tb_locals=False): if isinstance(module, str): self.module = __import__(module) for part in module.split('.')[1:]: self.module = getattr(self.module, part) else: self.module = module if argv is None: argv = sys.argv self.exit = exit self.failfast = failfast self.catchbreak = catchbreak self.verbosity = verbosity self.buffer = buffer self.tb_locals = tb_locals if warnings is None and not sys.warnoptions: # even if DeprecationWarnings are ignored by default # print them anyway unless other warnings settings are # specified by the warnings arg or the -W python flag self.warnings = 'default' else: # here self.warnings is set either to the value passed # to the warnings args or to None. # If the user didn't pass a value self.warnings will # be None. This means that the behavior is unchanged # and depends on the values passed to -W. self.warnings = warnings self.defaultTest = defaultTest self.testRunner = testRunner self.testLoader = testLoader self.progName = os.path.basename(argv[0]) self.parseArgs(argv) self.runTests()
在 unittest 模块中包含的 main 方法,可以方便地将测试模块变为可以运行的测试脚本。main 使用 unittest.TestLoader 类来自动查找和加载模块内的测试用例,TestProgram 类中该部分的代码如下:
def createTests(self, from_discovery=False, Loader=None): if self.testNamePatterns: self.testLoader.testNamePatterns = self.testNamePatterns if from_discovery: loader = self.testLoader if Loader is None else Loader() self.test = loader.discover(self.start, self.pattern, self.top) elif self.testNames is None: self.test = self.testLoader.loadTestsFromModule(self.module) else: self.test = self.testLoader.loadTestsFromNames(self.testNames, self.module)
在执行测试用例时,在 mian 方法中加入了 verbosity=2,代码如下:
unittest.main(verbosity=2)
verbosity 中默认是 1 ,0 代表执行的测试总数和全局结果,2 代表显示详细的信息。
verbosity=2 时,成功的测试用例会显示 OK,失败的测试用例会显示出详细的信息。
如果想单独地执行某一测试,用鼠标右键点击要执行的测试用例名称,选择“Run”。
三、构建测试套件
一个测试类中会有很多个测试用例,unittest 提供了“测试套件”方法,它由 unittest 模块中的 TestSuite 类表示,测试套件可以根据所测试的特性把测试用例组合在一起。
1、按顺序执行
TestSuite 类中提供了 addTest 方法可以实现测试用例按顺序执行,要执行的测试用例按自己期望执行顺序添加到测试套件中。
#-*-coding:utf-8-*- from selenium import webdriver import unittest class BaiduTest(unittest.TestCase): def setUp(self): self.driver = webdriver.Chrome() self.driver.maximize_window() self.driver.get('http://www.baidu.com') self.driver.implicitly_wait(30) def tearDown(self): self.driver.quit() def test_baidu_news(self): '''验证:百度首页点击新闻后的跳转''' self.driver.find_element_by_link_text('新闻').click() url = self.driver.current_url self.assertEqual(url,'http://news.baidu.com/') def test_baidu_map(self): '''验证:测试百度首页点击地图后的跳转''' self.driver.find_element_by_link_text('地图').click() self.driver.get('http://www,baidu.com') if __name__ == '__main__': '''使用addTest方法按顺序执行''' # suite = unittest.TestSuite() # suite.addTest(BaiduTest('test_baidu_news')) # suite.addTest(BaiduTest('test_baidu_map')) # unittest.TextTestRunner(verbosity=2).run(suite) '''使用makeSuite方法按测试类执行''' # suite = unittest.TestSuite(unittest.makeSuite(BaiduTest)) # unittest.TextTestRunner(verbosity=2).run(suite) '''使用TestLoader类来加载测试类''' suite = unittest.TestLoader().loadTestsFromTestCase('BaiduTest') unittest.TextTestRunner(verbosity=2).run()
使用addTest方法按顺序执行,注解:首先对 TestSuite 类进行实例化,使之成为一个对象 suite ,然后调用 TestSuite 类中的 addTest 方法,把测试用例添加到测试套件中,最后执行测试套件,从而执行测试套件中的测试用例。先添加的先执行。
2、按测试类执行
使用 makeSuite 可以实现把测试用例类中所有的测试用例组成测试套件 TestSuite,可以避免逐一向测试套件中添加测试用例。代码也在上面喔。
注解:在测试套件中 TestSuite 类中,unittest 模块调用了 makeSuite 的方法,makeSuite 方法的参数是,testCaseClass,也是测试类,如下:
def makeSuite(testCaseClass, prefix='test', sortUsing=util.three_way_cmp, suiteClass=suite.TestSuite): return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromTestCase( testCaseClass)
3、加载测试类
使用 TestLoader 类来加载测试类,并将测试用例返回添加到 TestSuite 中,代码也在上面喔。
4、按测试模块执行
在 TestLoader 类中也可以按模块来执行测试。python 中一个 .py 文件就是一个模块,一个模块可以有 N 个测试类,在一个测试类中可以有 N 个测试用例。如下:
'''一个Python文件就是一个模块,一个模块可以有N个测试类,在一个 测试类里可以有N个测试用例 ''' from selenium import webdriver import unittest class BaiduTest(unittest.TestCase): def setUp(self): self.driver = webdriver.Chrome() self.driver.implicitly_wait(30) self.driver.get('http://www.baidu.com') def test_title(self): '''验证:测试百度浏览器的tilte''' self.assertEqual(self.driver.title,'百度一下,你就知道') def test_so(self): '''验证:测试百度搜索输入框是否可以编辑''' so = self.driver.find_element_by_id('kw') self.assertTrue(so.is_enabled()) def test_002(self): '''验证:点击新闻''' self.driver.find_element_by_link_text('新闻').click() # @unittest.skip('do not run') def test_003(self): '''验证:点击百度地图''' self.driver.find_element_by_link_text('地图').click() def tearDown(self): self.driver.quit() class BaiduMap(unittest.TestCase): def setUp(self): self.driver = webdriver.Chrome() self.driver.maximize_window() self.driver.get('http://www.baidu.com') self.driver.implicitly_wait(30) def test_baidu_map(self): '''验证:测试百度首页点击地图后的跳转''' self.driver.find_element_by_link_text('地图').click() self.driver.get('http://www.baidu.com') def tearDown(self): self.driver.quit() if __name__ == '__main__': suite = unittest.TestLoader().loadTestsFromModule('按测试模块执行.py') unittest.TextTestRunner(verbosity=2).run(suite)
注解:模块是”按测试模块执行.py“,测试类分别是 BaiduTest 和 BaiduMap,这两个类执行不分先后,TestLoader 类直接调用 loadTestsFromModule 方法返回给指定模块中包含的所有参数用例套件。
5、优化测试套件
可以单独地把测试套件写成一个方法来调用。如下:
from selenium import webdriver import unittest '''这里以加载测试类为例,把测试类套件写成一个单独的方法''' class BaiduTest(unittest.TestCase): def setUp(self): self.driver = webdriver.Chrome() self.driver.maximize_window() self.driver.get('http://www.baidu.com') self.driver.implicitly_wait(30) def tearDown(self): self.driver.quit() def test_baidu_news(self): '''验证:测试百度首页点击新闻后跳转''' self.driver.find_element_by_link_text('新闻').click() self.assertEqual(self.driver.current_url,'http://news.baidu.com/') def test_baidu_map(self): '''验证:测试百度首页点击地图后的跳转''' self.driver.find_element_by_link_text('地图').click() self.assertEqual(self.driver.current_url,'https://map.baidu.com/@13376484.03,3517857.39,12z') @staticmethod def suite(testCaseClass): suite = unittest.TestLoader().loadTestsFromTestCase(testCaseClass) return suite if __name__ == '__main__': unittest.TextTestRunner(verbosity=2).run(BaiduTest.suite(BaiduTest))
四、分离测试固件
需要重复写的代码,可以把测试固件的这一部分分离出去,测试类直接继承分离出去的类。
把测试固件分离到 up_down.py 模块中,类名为 Up_Down,如下:
import unittest from selenium import webdriver class Up_Down(unittest.TestCase): def setUp(self): self.driver = webdriver.Chrome() self.driver.maximize_window() self.driver.get('http://www.baidu.com') self.driver.implicitly_wait(30) def tearDown(self): self.driver.quit()
测试类继承 Up_Down,在测试类中直接编写要执行的测试用例,如下:
import unittest from up_down import Up_Down class BaiduTest(Up_Down): def test_baidu_news(self): '''验证:百度首页点击新闻后的跳转''' self.driver.find_element_by_link_text('新闻').click() url = self.driver.current_url self.assertEqual(url,'http://news.baidu.com/') def test_baidu_map(self): '''验证:测试百度首页点击地图后的跳转''' self.driver.find_element_by_link_text('地图').click() self.driver.get('http://www,baidu.com') if __name__ == '__main__': unittest.main(verbosity=2)
注解:首先需要导入 up_down 模块中的 Up_Down 类,测试类 BaiduTest 继承 Up_Down 类,这样就和上面的执行顺序是一样的了,实现了固件分离。
五、测试断言(暂略)
六、批量执行测试用例
在 testCase 包中有 test_Up_Down.py 和 test套件.py 两个文件,创建 allTest.py 批量执行:
import unittest import os def allCases(): '''获取所有测试模块''' suite = unittest.TestLoader().discover( #批量获取测试模块,有三个参数 #文件路径,存放在testCase包中 start_dir= os.path.join(os.path.dirname(__file__)), pattern = 'test*.py',#所有以test开头的文件名 top_level_dir = None #直接给默认值None ) return suite if __name__ == '__main__': unittest.TextTestRunner(verbosity=2).run(allCases())
注解:discover 批量获取测试模块,有3个参数:
def discover(self, start_dir, pattern='test*.py', top_level_dir=None): 。。。
七、生成参数报告
借助第三方生成 HTML 格式的测试报告,使用到的库是 HTMLTestRunner.py。
下载地址:https://github.com/tungwaiyip/HTMLTestRunner,在 python3 中需要对代码进行修改。改错文档: https://www.cnblogs.com/testyao/p/5658200.html 文件如下:
""" A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance. The simplest way to use this is to invoke its main method. E.g. import unittest import HTMLTestRunner ... define your tests ... if __name__ == '__main__': HTMLTestRunner.main() For more customization options, instantiates a HTMLTestRunner object. HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. # output to a file fp = file('my_report.html', 'wb') runner = HTMLTestRunner.HTMLTestRunner( stream=fp, title='My unit test', description='This demonstrates the report output by HTMLTestRunner.' ) # Use an external stylesheet. # See the Template_mixin class for more customizable options runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' # run the test runner.run(my_test_suite) ------------------------------------------------------------------------ Copyright (c) 2004-2007, Wai Yip Tung All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Wai Yip Tung nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ # URL: http://tungwaiyip.info/software/HTMLTestRunner.html __author__ = "Wai Yip Tung" __version__ = "0.8.2" """ Change History Version 0.8.2 * Show output inline instead of popup window (Viorel Lupu). Version in 0.8.1 * Validated XHTML (Wolfgang Borgert). * Added description of test classes and test cases. Version in 0.8.0 * Define Template_mixin class for customization. * Workaround a IE 6 bug that it does not treat <script> block as CDATA. Version in 0.7.1 * Back port to Python 2.3 (Frank Horowitz). * Fix missing scroll bars in detail log (Podi). """ # TODO: color stderr # TODO: simplify javascript using ,ore than 1 class in the class attribute? import datetime import io import sys import time import unittest from xml.sax import saxutils # ------------------------------------------------------------------------ # The redirectors below are used to capture output during testing. Output # sent to sys.stdout and sys.stderr are automatically captured. However # in some cases sys.stdout is already cached before HTMLTestRunner is # invoked (e.g. calling logging.basicConfig). In order to capture those # output, use the redirectors for the cached stream. # # e.g. # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) # >>> class OutputRedirector(object): """ Wrapper to redirect stdout or stderr """ def __init__(self, fp): self.fp = fp def write(self, s): self.fp.write(s) def writelines(self, lines): self.fp.writelines(lines) def flush(self): self.fp.flush() stdout_redirector = OutputRedirector(sys.stdout) stderr_redirector = OutputRedirector(sys.stderr) # ---------------------------------------------------------------------- # Template class Template_mixin(object): """ Define a HTML template for report customerization and generation. Overall structure of an HTML report HTML +------------------------+ |<html> | | <head> | | | | STYLESHEET | | +----------------+ | | | | | | +----------------+ | | | | </head> | | | | <body> | | | | HEADING | | +----------------+ | | | | | | +----------------+ | | | | REPORT | | +----------------+ | | | | | | +----------------+ | | | | ENDING | | +----------------+ | | | | | | +----------------+ | | | | </body> | |</html> | +------------------------+ """ STATUS = { 0: 'pass', 1: 'fail', 2: 'error', } DEFAULT_TITLE = 'Unit Test Report' DEFAULT_DESCRIPTION = '' # ------------------------------------------------------------------------ # HTML Template HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>%(title)s</title> <meta name="generator" content="%(generator)s"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> %(stylesheet)s </head> <body> <script language="javascript" type="text/javascript"><!-- output_list = Array(); /* level - 0:Summary; 1:Failed; 2:All */ function showCase(level) { trs = document.getElementsByTagName("tr"); for (var i = 0; i < trs.length; i++) { tr = trs[i]; id = tr.id; if (id.substr(0,2) == 'ft') { if (level < 1) { tr.className = 'hiddenRow'; } else { tr.className = ''; } } if (id.substr(0,2) == 'pt') { if (level > 1) { tr.className = ''; } else { tr.className = 'hiddenRow'; } } } } function showClassDetail(cid, count) { var id_list = Array(count); var toHide = 1; for (var i = 0; i < count; i++) { tid0 = 't' + cid.substr(1) + '.' + (i+1); tid = 'f' + tid0; tr = document.getElementById(tid); if (!tr) { tid = 'p' + tid0; tr = document.getElementById(tid); } id_list[i] = tid; if (tr.className) { toHide = 0; } } for (var i = 0; i < count; i++) { tid = id_list[i]; if (toHide) { document.getElementById('div_'+tid).style.display = 'none' document.getElementById(tid).className = 'hiddenRow'; } else { document.getElementById(tid).className = ''; } } } function showTestDetail(div_id){ var details_div = document.getElementById(div_id) var displayState = details_div.style.display // alert(displayState) if (displayState != 'block' ) { displayState = 'block' details_div.style.display = 'block' } else { details_div.style.display = 'none' } } function html_escape(s) { s = s.replace(/&/g,'&'); s = s.replace(/</g,'<'); s = s.replace(/>/g,'>'); return s; } /* obsoleted by detail in <div> function showOutput(id, name) { var w = window.open("", //url name, "resizable,scrollbars,status,width=800,height=450"); d = w.document; d.write("<pre>"); d.write(html_escape(output_list[id])); d.write("\n"); d.write("<a href='javascript:window.close()'>close</a>\n"); d.write("</pre>\n"); d.close(); } */ --></script> %(heading)s %(report)s %(ending)s </body> </html> """ # variables: (title, generator, stylesheet, heading, report, ending) # ------------------------------------------------------------------------ # Stylesheet # # alternatively use a <link> for external style sheet, e.g. # <link rel="stylesheet" href="$url" type="text/css"> STYLESHEET_TMPL = """ <style type="text/css" media="screen"> body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; } table { font-size: 100%; } pre { } /* -- heading ---------------------------------------------------------------------- */ h1 { font-size: 16pt; color: gray; } .heading { margin-top: 0ex; margin-bottom: 1ex; } .heading .attribute { margin-top: 1ex; margin-bottom: 0; } .heading .description { margin-top: 4ex; margin-bottom: 6ex; } /* -- css div popup ------------------------------------------------------------------------ */ a.popup_link { } a.popup_link:hover { color: red; } .popup_window { display: none; position: relative; left: 0px; top: 0px; /*border: solid #627173 1px; */ padding: 10px; background-color: #E6E6D6; font-family: "Lucida Console", "Courier New", Courier, monospace; text-align: left; font-size: 8pt; width: 500px; } } /* -- report ------------------------------------------------------------------------ */ #show_detail_line { margin-top: 3ex; margin-bottom: 1ex; } #result_table { width: 80%; border-collapse: collapse; border: 1px solid #777; } #header_row { font-weight: bold; color: white; background-color: #777; } #result_table td { border: 1px solid #777; padding: 2px; } #total_row { font-weight: bold; } .passClass { background-color: #6c6; } .failClass { background-color: #c60; } .errorClass { background-color: #c00; } .passCase { color: #6c6; } .failCase { color: #c60; font-weight: bold; } .errorCase { color: #c00; font-weight: bold; } .hiddenRow { display: none; } .testcase { margin-left: 2em; } /* -- ending ---------------------------------------------------------------------- */ #ending { } </style> """ # ------------------------------------------------------------------------ # Heading # HEADING_TMPL = """<div class='heading'> <h1>%(title)s</h1> %(parameters)s <p class='description'>%(description)s</p> </div> """ # variables: (title, parameters, description) HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p> """ # variables: (name, value) # ------------------------------------------------------------------------ # Report # REPORT_TMPL = """ <p id='show_detail_line'>Show <a href='javascript:showCase(0)'>Summary</a> <a href='javascript:showCase(1)'>Failed</a> <a href='javascript:showCase(2)'>All</a> </p> <table id='result_table'> <colgroup> <col align='left' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> </colgroup> <tr id='header_row'> <td>Test Group/Test case</td> <td>Count</td> <td>Pass</td> <td>Fail</td> <td>Error</td> <td>View</td> </tr> %(test_list)s <tr id='total_row'> <td>Total</td> <td>%(count)s</td> <td>%(Pass)s</td> <td>%(fail)s</td> <td>%(error)s</td> <td> </td> </tr> </table> """ # variables: (test_list, count, Pass, fail, error) REPORT_CLASS_TMPL = r""" <tr class='%(style)s'> <td>%(desc)s</td> <td>%(count)s</td> <td>%(Pass)s</td> <td>%(fail)s</td> <td>%(error)s</td> <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td> </tr> """ # variables: (style, desc, count, Pass, fail, error, cid) REPORT_TEST_WITH_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'> <!--css div popup start--> <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > %(status)s</a> <div id='div_%(tid)s' class="popup_window"> <div style='text-align: right; color:red;cursor:pointer'> <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " > [x]</a> </div> <pre> %(script)s </pre> </div> <!--css div popup end--> </td> </tr> """ # variables: (tid, Class, style, desc, status) REPORT_TEST_NO_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'>%(status)s</td> </tr> """ # variables: (tid, Class, style, desc, status) REPORT_TEST_OUTPUT_TMPL = r""" %(id)s: %(output)s """ # variables: (id, output) # ------------------------------------------------------------------------ # ENDING # ENDING_TMPL = """<div id='ending'> </div>""" # -------------------- The end of the Template class ------------------- TestResult = unittest.TestResult class _TestResult(TestResult): # note: _TestResult is a pure representation of results. # It lacks the output and reporting ability compares to unittest._TextTestResult. def __init__(self, verbosity=1): TestResult.__init__(self) self.stdout0 = None self.stderr0 = None self.success_count = 0 self.failure_count = 0 self.error_count = 0 self.verbosity = verbosity # result is a list of result in 4 tuple # ( # result code (0: success; 1: fail; 2: error), # TestCase object, # Test output (byte string), # stack trace, # ) self.result = [] def startTest(self, test): TestResult.startTest(self, test) # just one buffer for both stdout and stderr self.outputBuffer = io.StringIO() stdout_redirector.fp = self.outputBuffer stderr_redirector.fp = self.outputBuffer self.stdout0 = sys.stdout self.stderr0 = sys.stderr sys.stdout = stdout_redirector sys.stderr = stderr_redirector def complete_output(self): """ Disconnect output redirection and return buffer. Safe to call multiple times. """ if self.stdout0: sys.stdout = self.stdout0 sys.stderr = self.stderr0 self.stdout0 = None self.stderr0 = None return self.outputBuffer.getvalue() def stopTest(self, test): # Usually one of addSuccess, addError or addFailure would have been called. # But there are some path in unittest that would bypass this. # We must disconnect stdout in stopTest(), which is guaranteed to be called. self.complete_output() def addSuccess(self, test): self.success_count += 1 TestResult.addSuccess(self, test) output = self.complete_output() self.result.append((0, test, output, '')) if self.verbosity > 1: sys.stderr.write('ok ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('.') def addError(self, test, err): self.error_count += 1 TestResult.addError(self, test, err) _, _exc_str = self.errors[-1] output = self.complete_output() self.result.append((2, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('E ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('E') def addFailure(self, test, err): self.failure_count += 1 TestResult.addFailure(self, test, err) _, _exc_str = self.failures[-1] output = self.complete_output() self.result.append((1, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('F ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('F') class HTMLTestRunner(Template_mixin): """ """ def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): self.stream = stream self.verbosity = verbosity if title is None: self.title = self.DEFAULT_TITLE else: self.title = title if description is None: self.description = self.DEFAULT_DESCRIPTION else: self.description = description self.startTime = datetime.datetime.now() def run(self, test): "Run the given test case or test suite." result = _TestResult(self.verbosity) test(result) self.stopTime = datetime.datetime.now() self.generateReport(test, result) # print >> sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime) print(sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)) return result def sortResult(self, result_list): # unittest does not seems to run in any particular order. # Here at least we want to group them together by class. rmap = {} classes = [] for n,t,o,e in result_list: cls = t.__class__ if not cls in rmap: rmap[cls] = [] classes.append(cls) rmap[cls].append((n,t,o,e)) r = [(cls, rmap[cls]) for cls in classes] return r def getReportAttributes(self, result): """ Return report attributes as a list of (name, value). Override this to add custom attributes. """ startTime = str(self.startTime)[:19] duration = str(self.stopTime - self.startTime) status = [] if result.success_count: status.append('Pass %s' % result.success_count) if result.failure_count: status.append('Failure %s' % result.failure_count) if result.error_count: status.append('Error %s' % result.error_count ) if status: status = ' '.join(status) else: status = 'none' return [ ('Start Time', startTime), ('Duration', duration), ('Status', status), ] def generateReport(self, test, result): report_attrs = self.getReportAttributes(result) generator = 'HTMLTestRunner %s' % __version__ stylesheet = self._generate_stylesheet() heading = self._generate_heading(report_attrs) report = self._generate_report(result) ending = self._generate_ending() output = self.HTML_TMPL % dict( title = saxutils.escape(self.title), generator = generator, stylesheet = stylesheet, heading = heading, report = report, ending = ending, ) self.stream.write(output.encode('utf8')) def _generate_stylesheet(self): return self.STYLESHEET_TMPL def _generate_heading(self, report_attrs): a_lines = [] for name, value in report_attrs: line = self.HEADING_ATTRIBUTE_TMPL % dict( name = saxutils.escape(name), value = saxutils.escape(value), ) a_lines.append(line) heading = self.HEADING_TMPL % dict( title = saxutils.escape(self.title), parameters = ''.join(a_lines), description = saxutils.escape(self.description), ) return heading def _generate_report(self, result): rows = [] sortedResult = self.sortResult(result.result) for cid, (cls, cls_results) in enumerate(sortedResult): # subtotal for a class np = nf = ne = 0 for n,t,o,e in cls_results: if n == 0: np += 1 elif n == 1: nf += 1 else: ne += 1 # format class description if cls.__module__ == "__main__": name = cls.__name__ else: name = "%s.%s" % (cls.__module__, cls.__name__) doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" desc = doc and '%s: %s' % (name, doc) or name row = self.REPORT_CLASS_TMPL % dict( style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', desc = desc, count = np+nf+ne, Pass = np, fail = nf, error = ne, cid = 'c%s' % (cid+1), ) rows.append(row) for tid, (n,t,o,e) in enumerate(cls_results): self._generate_report_test(rows, cid, tid, n, t, o, e) report = self.REPORT_TMPL % dict( test_list = ''.join(rows), count = str(result.success_count+result.failure_count+result.error_count), Pass = str(result.success_count), fail = str(result.failure_count), error = str(result.error_count), ) return report def _generate_report_test(self, rows, cid, tid, n, t, o, e): # e.g. 'pt1.1', 'ft1.1', etc has_output = bool(o or e) tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) name = t.id().split('.')[-1] doc = t.shortDescription() or "" desc = doc and ('%s: %s' % (name, doc)) or name tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL # o and e should be byte string because they are collected from stdout and stderr? if isinstance(o,str): # TODO: some problem with 'string_escape': it escape \n and mess up formating # uo = unicode(o.encode('string_escape')) # uo = o.decode('latin-1') uo = e else: uo = o if isinstance(e,str): # TODO: some problem with 'string_escape': it escape \n and mess up formating # ue = unicode(e.encode('string_escape')) # ue = e.decode('latin-1') ue = e else: ue = e script = self.REPORT_TEST_OUTPUT_TMPL % dict( id = tid, output = saxutils.escape(str(uo)+ue), ) row = tmpl % dict( tid = tid, Class = (n == 0 and 'hiddenRow' or 'none'), style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), desc = desc, script = script, status = self.STATUS[n], ) rows.append(row) if not has_output: return def _generate_ending(self): return self.ENDING_TMPL ############################################################################## # Facilities for running tests from the command line ############################################################################## # Note: Reuse unittest.TestProgram to launch test. In the future we may # build our own launcher to support more specific command line # parameters like test title, CSS, etc. class TestProgram(unittest.TestProgram): """ A variation of the unittest.TestProgram. Please refer to the base class for command line parameters. """ def runTests(self): # Pick HTMLTestRunner as the default test runner. # base class's testRunner parameter is not useful because it means # we have to instantiate HTMLTestRunner before we know self.verbosity. if self.testRunner is None: self.testRunner = HTMLTestRunner(verbosity=self.verbosity) unittest.TestProgram.runTests(self) main = TestProgram ############################################################################## # Executing this module from the command line ############################################################################## if __name__ == "__main__": main(module=None)
下载 HTMLTestRunner.py 文件后,把该文件放到 python 安装路径下的 Lib 子文件夹下面。创建 report 文件夹来放报告的,与 testCase 包放在同一个目录下。继续完善代码生成报告:
import unittest import os import time import HTMLTestRunner def allCases(): '''获取所有测试模块''' suite = unittest.TestLoader().discover( #批量获取测试模块,有三个参数 start_dir= os.path.join(os.path.dirname(__file__)),#文件路径,存放在testCase包中 pattern = 'test*.py',#所有以test开头的文件名 top_level_dir = None #直接给默认值None ) return suite def getNowTime(): '''获取当前时间''' #这里的时间是用来文件命名的,所以时间格式不能任意,要符合文件名的字符规范 return time.strftime('%Y-%m-%d %H-%M-%S',time.localtime(time.time())) def run(): # 通过文件路径获取文件名,可以 .xml或 .html格式等 fileName = os.path.join(os.path.dirname(__file__),'report',getNowTime()+'report.html')#通过文件路径获取文件名 fp = open(fileName,'wb')#特别注意文件名的字符规范 runer = HTMLTestRunner.HTMLTestRunner( stream=fp, verbosity=1, title='UI 自动化测试报告', description='UI 自动化测试报告详细信息' ) runer.run(allCases()) if __name__ == '__main__': run()
效果如下:
八、代码覆盖率统计
Coverage.py 是 python 程序代码覆盖率的测试工具,用于监视程序哪些代码执行了,哪些代码没有执行。
通过 pip3 install coverage 来安装,安装完成后,可以在模块路径下运行。文件下载地址:https://pypi.org/project/coverage/#files
如:查看 allTest.py 的代码覆盖率,cmd 命令框下输入 coverage3 run allTest.py,再执行 coverage html。
执行完后,同目录下会生成一个 htmlcov 文件夹,在该文件夹里显示的是代码覆盖率统计的文件,点击打开 index.html 文件,显示的是每个文件运行代码的覆盖率统计。
点击任意一个模块名可以查看具体的代码。
2020-03-22