一、摘要
本篇博文将介绍如何借助BeautifulReport和HTML模版,生成HTML测试报告的BeautifulReport 源码Clone地址为 https://github.com/TesterlifeRaymond/BeautifulReport,其中
BeautifulReport.py和其template是我们需要的关键。
二、BeautifulReport
如下代码是BeautifulReport.py的源码,其中几个注释的地方需要注意,将其集成进自己的自动化框架时需要做相应的修改
1import os 2import sys 3from io import StringIO as StringIO 4import time 5import json 6import unittest 7import platform 8import base64 9from distutils.sysconfig import get_python_lib 10import traceback 11from functools import wraps 12 13__all__ = ['BeautifulReport'] 14 15HTML_IMG_TEMPLATE = """ 16 <a href="data:image/png;base64, {}"> 17 <img src="data:image/png;base64, {}" width="800px" height="500px"/> 18 </a> 19 <br></br> 20""" 21 22 23class OutputRedirector(object): 24 """ Wrapper to redirect stdout or stderr """ 25 26 def __init__(self, fp): 27 self.fp = fp 28 29 def write(self, s): 30 self.fp.write(s) 31 32 def writelines(self, lines): 33 self.fp.writelines(lines) 34 35 def flush(self): 36 self.fp.flush() 37 38 39stdout_redirector = OutputRedirector(sys.stdout) 40stderr_redirector = OutputRedirector(sys.stderr) 41 42SYSSTR = platform.system() 43SITE_PAKAGE_PATH = get_python_lib() 44 45FIELDS = { 46 "testPass": 0, 47 "testResult": [ 48 ], 49 "testName": "", 50 "testAll": 0, 51 "testFail": 0, 52 "beginTime": "", 53 "totalTime": "", 54 "testSkip": 0 55} 56 57 58class PATH: 59 """ all file PATH meta """ 60 config_tmp_path = 'D:\\Programs\\Python\\PythonUnittest\\Template\\template' 61 62 63class MakeResultJson: 64 """ make html table tags """ 65 66 def __init__(self, datas: tuple): 67 """ 68 init self object 69 :param datas: 拿到所有返回数据结构 70 """ 71 self.datas = datas 72 self.result_schema = {} 73 74 def __setitem__(self, key, value): 75 """ 76 77 :param key: self[key] 78 :param value: value 79 :return: 80 """ 81 self[key] = value 82 83 def __repr__(self) -> str: 84 """ 85 返回对象的html结构体 86 :rtype: dict 87 :return: self的repr对象, 返回一个构造完成的tr表单 88 """ 89 keys = ( 90 'className', 91 'methodName', 92 'description', 93 'spendTime', 94 'status', 95 'log', 96 ) 97 for key, data in zip(keys, self.datas): 98 self.result_schema.setdefault(key, data) 99 return json.dumps(self.result_schema) 100 101 102class ReportTestResult(unittest.TestResult): 103 """ override""" 104 105 def __init__(self, suite, stream=sys.stdout): 106 """ pass """ 107 super(ReportTestResult, self).__init__() 108 self.begin_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 109 self.start_time = 0 110 self.stream = stream 111 self.end_time = 0 112 self.failure_count = 0 113 self.error_count = 0 114 self.success_count = 0 115 self.skipped = 0 116 self.verbosity = 1 117 self.success_case_info = [] 118 self.skipped_case_info = [] 119 self.failures_case_info = [] 120 self.errors_case_info = [] 121 self.all_case_counter = 0 122 self.suite = suite 123 self.status = '' 124 self.result_list = [] 125 self.case_log = '' 126 self.default_report_name = '自动化测试报告' 127 self.FIELDS = None 128 self.sys_stdout = None 129 self.sys_stderr = None 130 self.outputBuffer = None 131 132 @property 133 def success_counter(self) -> int: 134 """ set success counter """ 135 return self.success_count 136 137 @success_counter.setter 138 def success_counter(self, value) -> None: 139 """ 140 success_counter函数的setter方法, 用于改变成功的case数量 141 :param value: 当前传递进来的成功次数的int数值 142 :return: 143 """ 144 self.success_count = value 145 146 def startTest(self, test) -> None: 147 """ 148 当测试用例测试即将运行时调用 149 :return: 150 """ 151 unittest.TestResult.startTest(self, test) 152 self.outputBuffer = StringIO() 153 stdout_redirector.fp = self.outputBuffer 154 stderr_redirector.fp = self.outputBuffer 155 self.sys_stdout = sys.stdout 156 self.sys_stdout = sys.stderr 157 sys.stdout = stdout_redirector 158 sys.stderr = stderr_redirector 159 self.start_time = time.time() 160 161 def stopTest(self, test) -> None: 162 """ 163 当测试用力执行完成后进行调用 164 :return: 165 """ 166 self.end_time = '{0:.3} s'.format((time.time() - self.start_time)) 167 self.result_list.append(self.get_all_result_info_tuple(test)) 168 self.complete_output() 169 170 def complete_output(self): 171 """ 172 Disconnect output redirection and return buffer. 173 Safe to call multiple times. 174 """ 175 if self.sys_stdout: 176 sys.stdout = self.sys_stdout 177 sys.stderr = self.sys_stdout 178 self.sys_stdout = None 179 self.sys_stdout = None 180 return self.outputBuffer.getvalue() 181 182 def stopTestRun(self, title=None) -> dict: 183 """ 184 所有测试执行完成后, 执行该方法 185 :param title: 186 :return: 187 """ 188 FIELDS['testPass'] = self.success_counter 189 for item in self.result_list: 190 item = json.loads(str(MakeResultJson(item))) 191 FIELDS.get('testResult').append(item) 192 FIELDS['testAll'] = len(self.result_list) 193 FIELDS['testName'] = title if title else self.default_report_name 194 FIELDS['testFail'] = self.failure_count 195 FIELDS['beginTime'] = self.begin_time 196 end_time = int(time.time()) 197 start_time = int(time.mktime(time.strptime(self.begin_time, '%Y-%m-%d %H:%M:%S'))) 198 FIELDS['totalTime'] = str(end_time - start_time) + 's' 199 FIELDS['testError'] = self.error_count 200 FIELDS['testSkip'] = self.skipped 201 self.FIELDS = FIELDS 202 return FIELDS 203 204 def get_all_result_info_tuple(self, test) -> tuple: 205 """ 206 接受test 相关信息, 并拼接成一个完成的tuple结构返回 207 :param test: 208 :return: 209 """ 210 return tuple([*self.get_testcase_property(test), self.end_time, self.status, self.case_log]) 211 212 @staticmethod 213 def error_or_failure_text(err) -> str: 214 """ 215 获取sys.exc_info()的参数并返回字符串类型的数据, 去掉t6 error 216 :param err: 217 :return: 218 """ 219 return traceback.format_exception(*err) 220 221 def addSuccess(self, test) -> None: 222 """ 223 pass 224 :param test: 225 :return: 226 """ 227 logs = [] 228 output = self.complete_output() 229 logs.append(output) 230 if self.verbosity > 1: 231 sys.stderr.write('ok ') 232 sys.stderr.write(str(test)) 233 sys.stderr.write('\n') 234 else: 235 sys.stderr.write('.') 236 self.success_counter += 1 237 self.status = '成功' 238 self.case_log = output.split('\n') 239 self._mirrorOutput = True # print(class_name, method_name, method_doc) 240 241 def addError(self, test, err): 242 """ 243 add Some Error Result and infos 244 :param test: 245 :param err: 246 :return: 247 """ 248 logs = [] 249 output = self.complete_output() 250 logs.append(output) 251 logs.extend(self.error_or_failure_text(err)) 252 self.failure_count += 1 253 self.add_test_type('失败', logs) 254 if self.verbosity > 1: 255 sys.stderr.write('F ') 256 sys.stderr.write(str(test)) 257 sys.stderr.write('\n') 258 else: 259 sys.stderr.write('F') 260 261 self._mirrorOutput = True 262 263 def addFailure(self, test, err): 264 """ 265 add Some Failures Result and infos 266 :param test: 267 :param err: 268 :return: 269 """ 270 logs = [] 271 output = self.complete_output() 272 logs.append(output) 273 logs.extend(self.error_or_failure_text(err)) 274 self.failure_count += 1 275 self.add_test_type('失败', logs) 276 if self.verbosity > 1: 277 sys.stderr.write('F ') 278 sys.stderr.write(str(test)) 279 sys.stderr.write('\n') 280 else: 281 sys.stderr.write('F') 282 283 self._mirrorOutput = True 284 285 def addSkip(self, test, reason) -> None: 286 """ 287 获取全部的跳过的case信息 288 :param test: 289 :param reason: 290 :return: None 291 """ 292 logs = [reason] 293 self.complete_output() 294 self.skipped += 1 295 self.add_test_type('跳过', logs) 296 297 if self.verbosity > 1: 298 sys.stderr.write('S ') 299 sys.stderr.write(str(test)) 300 sys.stderr.write('\n') 301 else: 302 sys.stderr.write('S') 303 self._mirrorOutput = True 304 305 def add_test_type(self, status: str, case_log: list) -> None: 306 """ 307 abstruct add test type and return tuple 308 :param status: 309 :param case_log: 310 :return: 311 """ 312 self.status = status 313 self.case_log = case_log 314 315 @staticmethod 316 def get_testcase_property(test) -> tuple: 317 """ 318 接受一个test, 并返回一个test的class_name, method_name, method_doc属性 319 :param test: 320 :return: (class_name, method_name, method_doc) -> tuple 321 """ 322 class_name = test.__class__.__qualname__ 323 method_name = test.__dict__['_testMethodName'] 324 method_doc = test.__dict__['_testMethodDoc'] 325 return class_name, method_name, method_doc 326 327 328class BeautifulReport(ReportTestResult, PATH): 329 img_path = 'img/' if platform.system() != 'Windows' else 'img\\' 330 331 def __init__(self, suites): 332 super(BeautifulReport, self).__init__(suites) 333 self.suites = suites 334 self.log_path = None 335 self.title = '自动化测试报告' 336 self.filename = 'report.html' 337 338 def report(self, description, filename: str = None, log_path='.'): 339 """ 340 生成测试报告,并放在当前运行路径下 341 :param log_path: 生成report的文件存储路径 342 :param filename: 生成文件的filename 343 :param description: 生成文件的注释 344 :return: 345 """ 346 if filename: 347 self.filename = filename if filename.endswith('.html') else filename + '.html' 348 349 if description: 350 self.title = description 351 352 self.log_path = os.path.abspath(log_path) 353 self.suites.run(result=self) 354 self.stopTestRun(self.title) 355 self.output_report() 356 text = '\n测试已全部完成, 可前往{}查询测试报告'.format(self.log_path) 357 print(text) 358 359 def output_report(self): 360 """ 361 生成测试报告到指定路径下 362 :return: 363 """ 364 template_path = self.config_tmp_path 365 # template_path = "D:\\PythonUnittest\\Template\\template" 366 override_path = os.path.abspath(self.log_path) if \ 367 os.path.abspath(self.log_path).endswith('/') else \ 368 os.path.abspath(self.log_path) + '/' 369 370 with open(template_path, 'rb') as file: 371 body = file.readlines() 372 with open(override_path + self.filename, 'wb') as write_file: 373 for item in body: 374 if item.strip().startswith(b'var resultData'): 375 head = ' var resultData = ' 376 item = item.decode().split(head) 377 item[1] = head + json.dumps(self.FIELDS, ensure_ascii=False, indent=4) 378 item = ''.join(item).encode() 379 item = bytes(item) + b';\n' 380 write_file.write(item) 381 382 @staticmethod 383 def img2base(img_path: str, file_name: str) -> str: 384 """ 385 接受传递进函数的filename 并找到文件转换为base64格式 386 :param img_path: 通过文件名及默认路径找到的img绝对路径 387 :param file_name: 用户在装饰器中传递进来的问价匿名 388 :return: 389 """ 390 pattern = '/' if platform != 'Windows' else '\\' 391 392 with open(img_path + pattern + file_name, 'rb') as file: 393 data = file.read() 394 return base64.b64encode(data).decode() 395 396 def add_test_img(*pargs): 397 """ 398 接受若干个图片元素, 并展示在测试报告中 399 :param pargs: 400 :return: 401 """ 402 403 def _wrap(func): 404 @wraps(func) 405 def __wrap(*args, **kwargs): 406 img_path = os.path.abspath('{}'.format(BeautifulReport.img_path)) 407 try: 408 result = func(*args, **kwargs) 409 except Exception: 410 if 'save_img' in dir(args[0]): 411 save_img = getattr(args[0], 'save_img') 412 save_img(func.__name__) 413 data = BeautifulReport.img2base(img_path, pargs[0] + '.png') 414 print(HTML_IMG_TEMPLATE.format(data, data)) 415 sys.exit(0) 416 print('<br></br>') 417 418 if len(pargs) > 1: 419 for parg in pargs: 420 print(parg + ':') 421 data = BeautifulReport.img2base(img_path, parg + '.png') 422 print(HTML_IMG_TEMPLATE.format(data, data)) 423 return result 424 if not os.path.exists(img_path + pargs[0] + '.png'): 425 return result 426 data = BeautifulReport.img2base(img_path, pargs[0] + '.png') 427 print(HTML_IMG_TEMPLATE.format(data, data)) 428 return result 429 return __wrap 430 return _wrap
三、template
template文件是和BeautifulReport.py一起使用的,他将unittest的测试结果按照template的样式转换成HTML格式的报告
四、调用BeautifulReport
1import unittest 2from Run.BeautifulReport import BeautifulReport 3 4if __name__ == '__main__': 5 test_suite = unittest.defaultTestLoader.discover('TestScripts', pattern='test*.py') 6 result = BeautifulReport(test_suite) 7 result.report(filename='测试报告', description='测试报告', log_path='D:\\Programs\\Python\\PythonUnittest\\Reports')
五、报告样式
