很长时间以来,我们都需要针对每个版本出测试报告。尤其是在敏捷后,我们出具测试报告的频率会达到一周甚至更快,为了一定程度上解决这个问题,我打算做一个测试报告生成器。其实是很早也有这个想法,只是一直没能下手做。终于在昨天,我抽空做了一个能够适应我们当前测试现状的一个简易版本测试报告生成器,同时也将它分享出来,希望能够帮到有需要的同学。这个小工具比较简单,还希望各位大佬不要喷我哈。
最初的做次测试报告生成器的目的就是能够规范测试报告,即每个版本的测试报告结构固定;其次就算是能够自动根据测试数据渲染图表,不需要每次都去插入图标,调整格式;最后就是对于部分地方的数据能够进行个性化处理,比如加一些样式什么的;最终目的就算是能够实现一个轻量,简单的测试报告生成器。
最早也计划过使用前后端分离的模式开发一款能够进行数据驻留,多次编辑的测试报告生成器。但是鉴于时间原因,最终我选择了是使用一个超简单的且功能相对强的库Pywebio
来直接实现测试报告生成。它不但简单而且还可以方便我的同事对其进行优化。
PyWebIO
提供了一系列命令式的交互函数来在浏览器上获取用户输入和进行输出,将浏览器变成了一个“富文本终端”,可以用于构建简单的Web
应用或基于浏览器的GUI
应用。 使用PyWebIO
,开发者能像编写终端脚本一样(基于input
和print
进行交互)来编写应用,无需具备HTML
和JS
的相关知识;
PyWebIO
还可以方便地整合进现有的Web
服务。非常适合快速构建对UI
要求不高的应用。
特性
Flask、Django、Tornado、aiohttp、 FastAPI(Starlette)
框架集成最终,我对这个测试报告生成工具命名为TestReportGenerate
,简称TPG
,是一个Pywebio APP
,仅几百行代码。其以轻量,简单为初心,专注于测试报告生成。当然你也可以根据自己的需要进行修改和完善。
python3.7.5
pywebio==1.3.3
pyecharts==1.9.1
pandas==1.3.4
flask==1.1.2
openpyxl==3.0.7
鉴于源码太长,影响阅读体验,所以请大家后台回复
TPG
获取。
from pywebio.input import *
from pywebio.output import *
from pywebio.pin import *
from pyecharts import options as opts
from pyecharts.charts import Bar
from pyecharts.globals import ThemeType
from pywebio import start_server
from pywebio.platform import path_deploy,run_event_loop
import pandas as pd
import logging
handler = logging.FileHandler("tpg.log")
formatter= logging.Formatter("%(asctime)s - %(message)s")
handler.setFormatter(formatter)
def ReportGenerate():
basic_info = input_group('基础信息',[
input(
'请输入版本号',
name='version1',
type=TEXT,
placeholder='版本号,例:v3.0.1', # 占位
#value='v3.0.1',
#help_text='This is help text', # 提示
required=True, # 必填
datalist=['v3.0.1', 'v3.0.2', 'v3.0.3'] # 常驻输入联想
),
input(
'请输入迭代',
name='version2',
type=TEXT,
placeholder='迭代,例:1月迭代', # 占位
#value='1月迭代',
#help_text='This is help text', # 提示
required=True, # 必填
datalist=['1月迭代', '2月迭代', '3月迭代', '4月迭代', '5月迭代', '6月迭代', '7月迭代', '8月迭代', '9月迭代', '10月迭代', '11月迭代', '12月迭代'] # 常驻输入联想
),
input(
'请输入起始测试时间',
name='test_time1',
value='2022-2-0',
type=DATE,
required=True, # 必填
),
input(
'请输入结束测试时间',
name='test_time2',
value='2022-2-2',
type=DATE,
required=True, # 必填
),
input(
'请输入测试用例链接',
name='test_case_url',
type=URL,
required=True, # 必填
help_text='Jira上测试用例文档的链接',
),
])
issue_info = input_group('缺陷信息',[
input(
label='缺陷列表URL',
name='issue_url',
type=URL,
required=True,
),
input(
label='缺陷总数',
name='issue_total',
type=NUMBER,
required=True, # 必填
value=40
),
input(
label='致命的缺陷数',
name='die',
type=NUMBER,
required=True, # 必填
value=1
),
input(
label='严重的缺陷数',
name='critical',
type=NUMBER,
required=True, # 必填
value=4
),
input(
label='一般的缺陷数',
name='normal',
type=NUMBER,
required=True, # 必填
value=8
),
input(
label='建议的缺陷数',
name='low',
type=NUMBER,
required=True, # 必填
value=10
),
input(
label='关闭的缺陷数',
name='closed',
type=NUMBER,
required=True,
value=10
),
input(
label='拒绝的缺陷数',
name='refused',
type=NUMBER,
required=True,
value=1
),
# input(
# label='修复的缺陷数',
# name='fixed',
# type=NUMBER,
# required=True,
# value=1
# ),
input(
label='未解决的缺陷数',
name='error',
type=NUMBER,
required=True,
value=3
),
])
issue_analyse = input_group('缺陷分析',[
input(
label='模块1',
name='portal',
type=NUMBER,
required=True,
value=121
),
input(
label='模块2',
name='cmp',
type=NUMBER,
required=True,
value=22
),
input(
label='模块3',
name='nova',
type=NUMBER,
required=True,
value=12
),
input(
label='模块4',
name='net',
type=NUMBER,
required=True,
value=7
),
input(
label='模块5',
name='storage',
type=NUMBER,
required=True,
value=4
),
input(
label='模块6',
name='common',
type=NUMBER,
required=True,
value=9
),
input(
label='模块7',
name='product',
type=NUMBER,
required=True,
value=15
),
input(
label='模块8',
name='RDS',
type=NUMBER,
required=True,
value=18
),
input(
label='模块9',
name='arch',
type=NUMBER,
required=True,
value=0
),
input(
label='模块10',
name='devops',
type=NUMBER,
required=True,
value=1
),
input(
label='运维',
name='ops',
type=NUMBER,
required=True,
value=0
),
])
issue_analyse_2 = input_group('缺陷分析-致命&严重',[
input(
label='模块1',
name='portal',
type=NUMBER,
required=True,
value=1,
help_text='请填写致命+严重的缺陷数'
),
input(
label='模块2',
name='cmp',
type=NUMBER,
required=True,
value=1
),
input(
label='模块3',
name='nova',
type=NUMBER,
required=True,
value=1
),
input(
label='模块4',
name='net',
type=NUMBER,
required=True,
value=1
),
input(
label='模块5',
name='storage',
type=NUMBER,
required=True,
value=1
),
input(
label='模块6',
name='common',
type=NUMBER,
required=True,
value=1
),
input(
label='模块7',
name='product',
type=NUMBER,
required=True,
value=1
),
input(
label='模块8',
name='RDS',
type=NUMBER,
required=True,
value=1
),
input(
label='模块9',
name='arch',
type=NUMBER,
required=True,
value=0
),
input(
label='模块10',
name='devops',
type=NUMBER,
required=True,
value=0
),
input(
label='运维',
name='ops',
type=NUMBER,
required=True,
value=0
),
])
result_info = input_group('测试结论和建议',[
textarea(
label='测试结论',
value='根据测试数据分析,当前版本缺陷总数较多,严重缺陷相对较少,所有缺陷均已解决,版本质量良好。',
help_text='例:根据测试数据分析,当前版本缺陷总数较多,严重缺陷相对较少,所有缺陷均已解决,版本质量良好。',
name='res',
type=TEXT,
required=True,
rows=3,
),
input(
label='测试建议',
value='继续加强代码review管控,模块7设计评审。',
help_text='例:继续加强代码review管控,模块7设计评审。',
name='suggest',
type=TEXT,
required=True,
),
select(
label='发布结果',
value='可以发布',
options=['可以发布','不可发布'],
name='fb',
required=True,
),
input(
label='缺陷主要原因',
name='reason',
type=TEXT,
value='模块7设计缺失、需求不完整、代码BUG等导致',
required=True,
),
])
# =============== xlsx转换为html table================
files = file_upload("请上传未解决的缺陷文件.xlsx", accept=['.xlsx','xls'],)
pf = pd.read_excel(files.get('content'))
import time
times=time.strftime('%Y-%m-%d %T' , time.localtime())
test_time = '测试时间'
# ===============Part One=======================
put_markdown(f"# Python研究所 {basic_info['version1']} 版本 {basic_info['version2']} 测试报告").style('display:flex;justify-content: center;')
put_markdown(f"## 1、概述")
put_markdown(f"本测试报告为Python研究所{basic_info['version1']}版本功能测试报告;本报告目的在于评估{basic_info['version1']}/{basic_info['version2']}的基本功能是否满足发布要求。")
put_markdown("本报告预期参与人员包括测试执行人员、测试负责人员、项目管理人员、模块10人员和其他开发团队成员。")
# ===============Part Two=======================
put_markdown("## 2、测试参考文档")
put_column([
put_markdown("- [x] 需求文档"),
put_markdown("- [x] 概要设计"),
put_markdown("- [x] 详细设计"),
put_markdown("- [x] 环境部署文档"),
put_markdown("- [x] 自测报告"),
put_markdown("- [x] 转测申请"),
put_markdown("- [x] 测试计划"),
put_markdown("- [x] 测试用例"),
]
)
# ===============Part Three=======================
put_markdown("## 3、测试设计")
put_markdown("### 3.1、测试环境与配置")
put_column(
put_table([
[span('Python研究所',row=2), span('测试环境', col=2)],
['环境地址','规模'],
['IaaS', 'Python研究所','3控+3管+4模块3'],
['Portal', 'Python研究所', 'xx'],
['模块2', 'Python研究所', 'xx'],
])).style('display:flex;justify-content: center;')
put_markdown("### 3.2、测试用例设计")
put_markdown("测试用例的设计按场景、模块等多个层次进行用例设计。通过基础核心功能、基础非核心功能以及异常场景下的功能可用性将用例分为了P0、P1、P2、P3四种级别。")
put_markdown(" - P0:基础核心功能")
put_markdown(" - P1:基础高频功能")
put_markdown(" - P2:基础低频功能")
put_markdown(" - P3:UI布局&用户体验")
put_markdown("### 3.3、测试方法")
put_markdown("本次测试采用黑盒测试方法。通过手动执行功能测试用例结合自动化回归进行测试。")
# ===============Part Four=======================
put_markdown("## 4、测试计划")
put_markdown("### 4.1、测试时间和人员")
put_markdown(f" > 测试时间:{basic_info['test_time1']}-{basic_info['test_time2']}")
# put_table([
# ['模块','模块3','模块5','模块4','模块8','SLB','审计','组织','运营-模块7','运营-计费','运营-其他','模块1-其他'],
# ['主测试','Python研究所','Python研究所','Python研究所','Python研究所','phyger','Python研究所','Python研究所','Python研究所','Python研究所','Python研究所','all',],
# ['备测试','all','phyger','phyger','phyger','Python研究所','Python研究所','all','Python研究所','Python研究所','all','all']
# ])
put_table([
['模块3', 'Python研究所', 'all'],
['模块5', 'Python研究所', 'phyger'],
['模块4', 'Python研究所', 'phyger'],
['模块8', 'Python研究所', 'phyger'],
['ok', 'phyger', 'Python研究所'],
['ko', 'Python研究所', 'Python研究所'],
['xx', 'Python研究所', 'all'],
['运营-模块7', 'Python研究所', 'Python研究所'],
['运营-计费', 'Python研究所', 'Python研究所'],
['运营-其他', 'Python研究所', 'all'],
['模块1-其他', 'all', 'all'],
], header=['模块', '主测试', '备测试'])
put_markdown("### 4.2、功能覆盖")
put_link(name="1. ★历史版本存量功能",url='https://app.rwork.crc.com.cn/mindnotes/bmnk9UIti5nJoDiBJN46F1w4DQb',new_window=True)
put_html('<br>')
put_link(name=f"2. ★{basic_info['version2']}新增需求",url='https://steam.crcloud.com/3/project/436/#/issue',new_window=True)
put_markdown("### 4.3、被测环境结构")
put_table([
['portal','aaaa','xxxxx'],
['middle','a+a','xx平台,xx和模块1的桥梁'],
[span('模块1',row=13), 'xxx','xxx生命周期管理'],
['aa', 'aaaa'],
['xx', 'xxxx'],
],header=['分类','模块','概述'])
put_markdown("### 4.4、测试用例")
put_link(name='点击查看测试用例',url=basic_info['test_case_url'],new_window=True)
# ===============Part Five======================
put_markdown("## 5、缺陷分析")
put_markdown("### 5.1、缺陷概览")
put_markdown(f"致命缺陷数:{issue_info['die']}").style('color: red;')
put_markdown(f"严重缺陷数:{issue_info['critical']}").style('color: #ff00ff;')
# ===============Picture======================
c = (
Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT))
.add_xaxis([
'已拒绝',
'已解决',
'未解决',
'缺陷总数'
])
.add_yaxis("", [
issue_info['refused'],
issue_info['closed'],
issue_info['error'],
issue_info['issue_total']
])
.set_global_opts(
xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=0)),
title_opts=opts.TitleOpts(title="缺陷-按状态分布"))
)
c.width = "100%"
put_html(c.render_notebook())
# ===============Picture======================
c = (
Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT))
.add_xaxis([
'致命',
'严重',
'一般',
'建议'
])
.add_yaxis("", [
issue_info['die'],
issue_info['critical'],
issue_info['normal'],
issue_info['low']
])
.set_global_opts(
xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=0)),
title_opts=opts.TitleOpts(title="缺陷-按严重程度分布"))
)
c.width = "100%"
put_html(c.render_notebook())
# put_table([
# [issue_info['critical'], issue_info['closed'], issue_info['refused'],issue_info['fixed'],issue_info['error'],issue_info['issue_total']],
# ], header=['严重缺陷', '已关闭','已拒绝','已修复','未解决','缺陷总数'])
put_markdown(f"本次测试总共发现问题总计{issue_info['issue_total']}个,已经关闭{issue_info['closed']}个,关闭率为{float('%.2f' %(issue_info['closed']/issue_info['issue_total']*100))}%,还剩余{issue_info['error']}个缺陷未处理。缺陷发生的主要原因为:{result_info['reason']}。")
put_markdown("### 5.2、缺陷分析")
put_markdown("#### 5.2.1、缺陷-按模块分布")
# ===============Bar Picture 缺陷分布 =================
c = (
Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT))
.add_xaxis([
'模块1',
'模块2',
'模块3',
'模块4',
'模块5',
'模块6',
'模块7',
'模块8',
'模块9',
'模块10',
'ops'
])
.add_yaxis("总缺陷", [
issue_analyse['portal'],
issue_analyse['cmp'],
issue_analyse['nova'],
issue_analyse['net'],
issue_analyse['storage'],
issue_analyse['common'],
issue_analyse['product'],
issue_analyse['RDS'],
issue_analyse['arch'],
issue_analyse['devops'],
issue_analyse['ops']
])
.set_global_opts(
xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=-15)),
title_opts=opts.TitleOpts(title="缺陷-按模块分布"))
)
c.width = "100%"
put_html(c.render_notebook())
# ===============Bar Picture 严重缺陷分布 =================
put_markdown("#### 5.2.2、严重缺陷-按模块分布")
c = (
Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT))
.add_xaxis([
'模块1',
'模块2',
'模块3',
'模块4',
'模块5',
'模块6',
'模块7',
'模块8',
'模块9',
'模块10',
'ops'
])
.add_yaxis("严重缺陷", [
issue_analyse_2['portal'],
issue_analyse_2['cmp'],
issue_analyse_2['nova'],
issue_analyse_2['net'],
issue_analyse_2['storage'],
issue_analyse_2['common'],
issue_analyse_2['product'],
issue_analyse_2['RDS'],
issue_analyse_2['arch'],
issue_analyse_2['devops'],
issue_analyse_2['ops']
])
.set_global_opts(
xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=-15)),
title_opts=opts.TitleOpts(title="严重缺陷-按模块分布"))
)
c.width = "100%"
put_html(c.render_notebook())
put_markdown("#### 5.2.3、未修复的缺陷列表")
put_html(pf.to_html(border=0))
put_markdown("### 5.3、缺陷列表")
put_link(name='点击查看缺陷列表',url=issue_info['issue_url'],new_window=True)
put_markdown("## 6、测试结论和建议")
put_markdown(f"{ result_info['res']}缺陷发生的主要原因为:{result_info['reason']}。")
put_markdown(f"{ result_info['suggest']}")
if result_info['fb']=='可以发布':
put_markdown(f"当前版本质量状态:{ result_info['fb'] }").style('color: #00ff00')
else:
put_markdown(f"当前版本质量状态:{ result_info['fb'] }").style('color: #ff0000')
#put_markdown(f"本次测试总共发现问题总计{issue_info['issue_total']}个,已经关闭{issue_info['closed']}个,关闭率为{float('%.2f' %(issue_info['closed']/issue_info['issue_total']*100))}%,还剩余{issue_info['error']}个缺陷未处理。缺陷发生的主要原因为:{issue_info['reason']}。")
put_markdown("## 7、附录")
put_markdown("### 7.1、缺陷状态定义")
put_table([
['新建','测试人员测试过程中发现的,认为其可能会影响到系统功能,性能,用户体验等的问题,同时将其提出'],
['处理中','测试人员认为是系统缺陷或者是需要对系统进行优化,开发人员确认并且开始处理'],
['已修复','开发人员已经针对问题提出修复方案并且完成实施,自验通过'],
['已拒绝','问题的分析者认为不是缺陷,经问题提出者确认可拒绝'],
['关闭','缺陷确认者(一般为问题生成人)验证后认为问题已解决属实,并且回归通过'],
], header=['缺陷状态','状态释义'])
put_markdown("### 7.2、缺陷严重程度定义")
put_table([
['致命','确认或者基本确认会影响到系统核心功能或者系统稳定的问题'],
['严重','确认会影响到环境基础功能的问题'],
['一般','确认会影响到系统主要功能,或者在异常场景下会造成系统异常的问题'],
['建议','不影响系统功能,但影响系统的易用性(如界面美观问题、操作建议等)或产出物的一些非技术性质量问题(如文档版本、错别字等)'],
], header=['严重等级','等级释义'])
put_text(f"Copyright © Phyger | Generate at:{times}").style('display:flex;justify-content: center;')
from pywebio.platform.flask import webio_view
from flask import Flask
app = Flask(__name__)
# `task_func` is PyWebIO task function
app.add_url_rule('/', 'webio_view', webio_view(ReportGenerate),
methods=['GET', 'POST', 'OPTIONS']) # need GET,POST and OPTIONS methods
if __name__=='__main__':
app.logger.addHandler(handler)
app.run(host='0.0.0.0', port=8989)
这个工具非常简单,但是目前使用还是很方便的。同时它也有很多优化的空间,比如输入可以做成那种 form
表单,支持上一步数据保留,报告中的数据从测试系统自动拉取等等。期待 TPG
能够越来越好。