欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

pythonunittest+BSTestRunner生成测试报告与unittest执行测试用例封装

时间:2023-05-19
python unittest+BSTestRunner生成测试报告与unittest执行测试用例封装

参考框架时unittest与pytest对比后,思考还是用unittest来做接口自动化框架,但是unittest内不带html的报告,需要添加报告模块,网上有HtmlTestRunner也是基于runner来开发的,但后来又找到了另一个html的模块BSTestRunner新建python.py文件,把代码粘贴在内,放在lib/python/site-packages的根目录下

"""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 BSTestRunner ..、define your tests ... if __name__ == '__main__': BSTestRunner.main()For more customization options, instantiates a BSTestRunner object.BSTestRunner is a counterpart to unittest's TextTestRunner、E.g. # output to a file fp = file('my_report.html', 'wb') runner = BSTestRunner.BSTestRunner( stream=fp, title='My unit test', description='This demonstrates the report output by BSTestRunner.' ) # Use an external stylesheet. # See the Template_mixin class for more customizable options runner.STYLESHEET_TMPL = '' # run the test runner.run(my_test_suite)------------------------------------------------------------------------Copyright (c) 2004-2007, Wai Yip TungCopyright (c) 2016, Eason HanAll rights reserved.Redistribution and use in source and binary forms, with or withoutmodification, are permitted provided that the following conditions aremet:* 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 "ASIS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITEDTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR APARTICULAR PURPOSE ARE DISCLAIMED、IN NO EVENT SHALL THE COPYRIGHT OWNEROR 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, ORPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OFLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDINGNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THISSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."""__author__ = "Wai Yip Tung && Eason Han"__version__ = "0.8.4""""Change HistoryVersion 0.8.3* Modify html style using bootstrap3.Version 0.8.3* Prevent crash on class or module-level exceptions (Darren Wurf).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 %(heading)s %(report)s %(ending)s""" # variables: (title, generator, stylesheet, heading, report, ending) # ------------------------------------------------------------------------ # Stylesheet # # alternatively use a for external style sheet, e.g. # STYLESHEET_TMPL = """""" # ------------------------------------------------------------------------ # Heading # HEADING_TMPL = """

%(title)s

%(parameters)s

%(description)s

""" # variables: (title, parameters, description) HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

""" # variables: (name, value) # ------------------------------------------------------------------------ # Report # REPORT_TMPL = """

SummaryFailedAll

Test Group/Test case Count Pass Fail Error View %(test_list)s Total %(count)s %(Pass)s %(fail)s %(error)s """ # variables: (test_list, count, Pass, fail, error) REPORT_CLASS_TMPL = r""" %(desc)s %(count)s %(Pass)s %(fail)s %(error)s Detail""" # variables: (style, desc, count, Pass, fail, error, cid) REPORT_TEST_WITH_OUTPUT_TMPL = r""" %(desc)s %(status)s [x]
        %(script)s        

""" # variables: (tid, Class, style, desc, status) REPORT_TEST_NO_OUTPUT_TMPL = r""" %(desc)s %(status)s""" # variables: (tid, Class, style, desc, status) REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s""" # variables: (id, output) # ------------------------------------------------------------------------ # ENDING # ENDING_TMPL = """"""# -------------------- The end of the Template class -------------------TestResult = unittest.TestResultclass _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.outputBuffer = StringIO() 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 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('../../TestLoader/TestCase') 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 BSTestRunner(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) sys.stderr.write('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 rmap.has_key(cls): 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 = 'BSTestRunner %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, ) try: self.stream.write(output.encode('utf8')) except: self.stream.write(output) 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), name = name, value = 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 'text text-warning' or nf > 0 and 'text text-danger' or 'text text-success', 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')) try: uo = o.decode('latin-1') except: uo = o 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')) try: ue = e.decode('latin-1') except: ue = e else: ue = e script = self.REPORT_TEST_OUTPUT_TMPL % dict( id = tid, output = saxutils.escape(uo+ue), ) row = tmpl % dict( tid = tid, # Class = (n == 0 and 'hiddenRow' or 'none'), Class = (n == 0 and 'hiddenRow' or 'text text-success'), # style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), style = n == 2 and 'text text-warning' or (n == 1 and 'text text-danger' or 'text text-success'), 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 BSTestRunner as the default test runner. # base class's testRunner parameter is not useful because it means # we have to instantiate BSTestRunner before we know self.verbosity. if self.testRunner is None: self.testRunner = BSTestRunner(verbosity=self.verbosity) unittest.TestProgram.runTests(self)main = TestProgram############################################################################### Executing this module from the command line##############################################################################if __name__ == "__main__": main(module=None)

执行main()方法查看
unittest执行测试case有三种方式
第一种 unittest.main()方法 执行当前py文件内所有case

# -*- coding: utf-8 -*-import unittestfrom TestLoader.TestCase.methods.spring_festival import SpringFestivalMethodsfrom backstage.account_access import AccountAccessfrom backstage.activity_general import AbnormalCaseclass test_sample(unittest.TestCase): def __init__(self, *args, **kwargs): super(test_sample, self).__init__(*args, **kwargs) self.A = 107205623 self.code = 692 self.lucky_attrs = { "lotteryType": 1, "lotteryFlag": 1 } self.physical_attrs = { "count": 1 } self.attrs = { "costType": 1, "count": 10 } def test_case_01(self): """页面信息""" response = SpringFestivalMethods().getLotteryInfo(self.A) def test_case_02(self): """刷新气泡""" response = SpringFestivalMethods().refreshUnRecvCount(self.A) def test_case_03(self): """实验抽奖异常case脚本""" AccountAccess().add_ticket(self.A) AbnormalCase(self.A, self.code, "lottery", **self.lucky_attrs).run(1) def test_case_04(self): """实验体力异常case脚本""" AbnormalCase(self.A, self.code, "diamonChange", **self.physical_attrs).run(2) def test_case_05(self): """实验体力异常case脚本""" AbnormalCase(self.A, 544, "exchangeConpon", **self.attrs).run(3)if __name__ == '__main__': unittest.main()

第二种:执行测试用例文件
新建run.py文件写入执行case方法

用遍历list的方式执行多个py文件,在方法内加入生成测试报告方法BSTestRunner()
返回的result在后续的钉钉机器人处理时会用到参数

def run_py(list_file): """执行多个测试用例文件""" test_dir = '../TestLoader' report_dir = './report' for i in list_file: discover = unittest.defaultTestLoader.discover(test_dir, pattern=i) # 定义报告格式 now = time.strftime('%Y-%m-%d %H_%M_%S') report_name = report_dir + '/' + now + 'test_report.html' # 运行用例并生成测试报告 with open(report_name, 'wb') as f: runner = BSTestRunner(stream=f, title="测试case内容", description="测试报告") result = runner.run(discover) logger.info("n=====生成测试报告=====") return resultif __name__ == '__main__': file_list = ["test_shiyan.py", "MeetSpringFestival.py"] run_py(file_list)

第三种:执行文件内的单独case

def run_case(test_py, list_case): """执行一个文件下指定case内容""" report_dir = './report' # 1、构造用例集 suite = unittest.TestSuite() # 2、执行顺序是安加载顺序:先执行test_case_01,再执行test_case_02 for i in list_case: suite.addTest(test_py(i)) # 定义报告格式 now = time.strftime('%Y-%m-%d %H_%M_%S') report_name = report_dir + '/' + now + 'test_report.html' # 运行用例并生成测试报告 with open(report_name, 'wb') as f: runner = BSTestRunner(stream=f, title="测试case内容", description="测试报告") result = runner.run(suite) logger.info("n=====生成测试报告=====") return resultif __name__ == '__main__': case_list = ["test_case_01", "test_case_02"] # run_case()第一个参数是py文件的名称,第二个是case的list run_case(test_sample, case_list)

执行后,测试报告会保存在方法内定义的report文件内

html报告展示效果

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。