一、摘要

本篇博文将介绍如何借助BeautifulReport和HTML模版,生成HTML测试报告的BeautifulReport 源码Clone地址为 https://github.com/TesterlifeRaymond/BeautifulReport,其中

BeautifulReport.py和其template是我们需要的关键。

二、BeautifulReport

如下代码是BeautifulReport.py的源码,其中几个注释的地方需要注意,将其集成进自己的自动化框架时需要做相应的修改

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

三、template

template文件是和BeautifulReport.py一起使用的,他将unittest的测试结果按照template的样式转换成HTML格式的报告

四、调用BeautifulReport

  1. import unittest
  2. from Run.BeautifulReport import BeautifulReport
  3.  
  4. if __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')

五、报告样式

Python&Selenium&Unittest&BeautifuReport 自动化测试并生成HTML自动化测试报告的更多相关文章

  1. Java&Selenium&TestNG&ZTestReport 自动化测试并生成HTML自动化测试报告

    一.摘要 本篇博文将介绍如何借助ZTestReport和HTML模版,生成HTML测试报告的ZTestReport 源码Clone地址为 https://github.com/zhangfei1984 ...

  2. python+selenium+unittest 实现自动化测试

    示例代码: baidu.py import csv #导入csv模块 from itertools import islice #从itertools导入islice,后边让其默认跳过第一行使用 fr ...

  3. Python+Selenium+Unittest+Ddt+HTMLReport分布式数据驱动自动化测试框架结构

    1.Business:公共业务模块,如登录模块,可以把登录模块进行封装供调用 ------login_business.py from Page_Object.Common_Page.login_pa ...

  4. Python+selenium+unittest+HTMLTestReportCN单元测试框架分享

    分享一个比较基础的,系统性的知识点.Python+selenium+unittest+HTMLTestReportCN单元测试框架分享 Unittest简介 unittest是Python语言的单元测 ...

  5. Python HTMLTestRunner生成网页自动化测试报告时中文编码报错UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6

    1. 由于使用Python Selenium做网页自动化测试时,有截取网页上的中文信息保存到测试结果中,最终出现编码错误如下: File "D:/PycharmProjects/AutoTe ...

  6. python+selenium +unittest生成HTML测试报告

    python+selenium+HTMLTestRunner+unittest生成HTML测试报告 首先要准备HTMLTestRunner文件,官网的HTMLTestRunner是python2语法写 ...

  7. Python Selenium unittest+HTMLTestRunner实现 自动化测试及发送测试报告邮件

    1.UI测试框架搭建-目录结构 2. 文件介绍 2.1.baseinfo->__init__.py 配置文件定义基础参数 #-*-coding:utf-8-*- #测试用例配置参数 base_u ...

  8. Python+appium+unittest UI自动化测试

    什么是UI自动化 自动化分层 单元自动化测试,指对软件中最小可测试单元进行检查和验证,一般需要借助单元测试框架,如java的JUnit,python的unittest等 接口自动化测试,主要检查验证模 ...

  9. Python单元测试unittest与HTMLTestRunner报告生成

    本文为简单介绍,使用python自带模块unittest来进行单元测试 首先我们有一个需要测试的类,employee.py  定义了涨薪的方法.我们需要测试这个类的功能是否正确. class Empl ...

随机推荐

  1. java、python、golang等开发语言如何快速生成二维码?

    免费二维码生成途径非常多!比如比较有名的草料二维码,如果只是简单的使用,用它就足够了.但是如果想大规模的生成,那就不太合适了.再者很多工具都没办法在二维码中加入logo(像微信二维码一样). 接下来, ...

  2. Python Elasticsearch

    以下所用版本为Elasticsearch 7.2.0 1.安装 pip3 install elasticsearch -i https://pypi.tuna.tsinghua.edu.cn/simp ...

  3. EMR-LDAP配置

    usersync是负责在配置policy的时候可选用户有ldap里的用户,admin是负责登录webui的 https://cwiki.apache.org/confluence/display/RA ...

  4. SQL SERVER 字符串函数 REPLACE()

    定义: REPLACE()返回用另一个字符串值替换原字符串中出现的所有指定字符串值之后的字符串. 语法: REPLACE ( string_expression , string_pattern , ...

  5. tabs 导航 及内容切换

    <!-- 导航头 --> <div class="col-md-6" style="padding: 0px"> <ul id=& ...

  6. 在django中进行后台管理时插入外键数据时不显示值的问题

    在django的后台管理站点插入数据时,发现需要添加外键时,下拉框中不显示值 按照显示内容中的object,考虑这里应该是调用的模型类的objects对象方法,那么去models.py中对模型类添加一 ...

  7. APM之原理篇

    APM,应用性能监控,有new relic等产品,对APM感兴趣的应该不会不知道它了.主要功能就是统计分析应用的CPU.内存.网络.数据库.UI等性能,并提供错误日志捕获.编码人员需要做的仅仅是使用它 ...

  8. Java web server 基本实现原理

    public class WebServer { //服务端Socket只要一个,所以定义成static, 同一时间只能一个线程访问(主线程) private static ServerSocket ...

  9. Dijstra_优先队列_前向星

    Dijstra算法求最短路径 具体实现方式 设置源点,将源点从原集u{}中取出并放入新建集s{} 找出至源点最近的点q从原集取出放入新集s{} 由q点出发,更新所有由q点能到达的仍处于原集的点到源点的 ...

  10. PHP获取今日、昨日、本周、上周、本月、上月、本季、上季、今年、去年

    //今天开始$beginToday = date('Y-m-d 00:00:00', time());//今天结束$endToday = date('Y-m-d 23:59:59', time()); ...