1、任务概述
本次计划的核心任务是开发一个,个人版的公共用例库,旨在将各系统和各类测试场景下的通用、基础以及关键功能的测试用例进行系统性地归纳整理,并以提高用例的复用率为目标,力求最大限度地减少重复劳动,提升测试效率。
计划内容:完成公共用例库的开发实施工作,包括需求分析、系统设计、开发、测试、打包、运行维护等工作。
1.1、 已完成:
需求分析、数据库表的设计:公共用例库计划–个人版(一)
主体界面与逻辑设计:公共用例库计划–个人版(二)
导出Excel功能:公共用例库计划–个人版(三)
模块选择功能改造与性能优化公共用例库计划–个人版(四)
QtCharts制作首页饼图与柱状图公共用例库计划–个人版(五)
典型Bug页面设计与开发公共用例库计划–个人版(六)
1.2、 本次待完成:
导入导出页面:选择模块,把用例导出为Excel文件。导入:模板中填写用例与模块信息,导入库中。
1. 导出模板改造
2. Excel导入用例
导入导出完成后,公共用例库的主体功能就开发完成了。剩下部分功能的优化,还有测试。同时进行界面的美化,打包。
2、导出功能改造
主要用到的库
PyQt6
openpyxl
datetime
2.1 界面
界面增加了按钮图标。还是点击选择模块,选择路径后,点击导出。
2.2 导出bug修改
1、bug情况描述:选择模块后查询,页面显示没有模块选中,点击导出却导出了用例。
分析:是模块查询没有清空已选模块的ID集合,导出了查询前的选择模块。
解决:将self.selected_module_values集合定义,从页面初始化移动到模块查询中。这样页面初始化会查询模块,可以定义集合,查询操作也可以清空集合。
def mkchaxun_down(self):
"""导出,模块查询"""
self.selected_module_values=set() # 导出模块选择
vlue=('%' + self.mkcx_3.text() + '%', 10)
row, count=self.mk_selecetsql(vlue)
self.mkpailie(row, self.mkliebiao_3)
self.label_19.setText(f"模块数: {count[0]}")
logging.info('导出,模块查询')
2、bug情况描述:点击全选后,一个取消选中,但是模块全选还是勾选。
分析:取消选中只删除了选中模块ID,没有对全选checkBox进行重置。
解决:在取消选中,删除模块ID的语句后面,增加:self.checkBox_2.setChecked(False) # 取消全选
def on_item_clicked_down(self, item, column):
"""导出功能,模块及其子模块选中或者取消选中"""
parent_check_state=Qt.CheckState.Unchecked if item.checkState(
column) == Qt.CheckState.Checked else Qt.CheckState.Checked
item.setCheckState(column, parent_check_state) # 设置当前项的勾选状态
if item.checkState(column) == Qt.CheckState.Checked:
self.selected_module_values.add(int(item.text(1))) # 如果当前项被选中,则添加到 selected_module_values 集合
else:
try:
self.selected_module_values.remove(int(item.text(1))) # 如果当前项被取消选中,则从 selected_module_values 集合中移除
self.checkBox_2.setChecked(False) # 取消全选
except KeyError:
pass
def set_child_items_check_state(parent_item):
for i in range(parent_item.childCount()):
child_item=parent_item.child(i)
child_item.setCheckState(column, parent_check_state)
if child_item.checkState(column) == Qt.CheckState.Checked: # 根据子项的新勾选状态处理列表
self.selected_module_values.add(int(child_item.text(1)))
else:
try:
self.selected_module_values.remove(int(child_item.text(1)))
except KeyError:
pass
set_child_items_check_state(child_item) # 递归调用
2.3 导出模版改造
之前的导出:用例导出,sql查询用例,新建一个Excel同时写入。模板导出,会新建另外一个Excel模板,然后保存到文件夹。
问题:有两段新建Excel的代码,冗余。导入,导出的Excel就不匹配,一致性不好。
优化:定义一个新建模板的函数,用例导出:sql查询后,新建模板,打开模板写入。
2.3.1 导出按钮
判断导出模块等条件后,将按钮失效,进行查询,如果查询到用例,就新建Excel,写入Excel。
def download(self):
"""导出用例按钮"""
if not self.selected_module_values:
self.ts.xinxi("请选中模块")
elif self.geshi.currentIndex() == 1:
self.ts.xinxi("暂不支持格式")
else:
self.daochu.setEnabled(False) # 导出按钮置灰
items, items2=self.download_sql() # 查询导出用例、模块
self.daochulog.clear() # 清空说明
self.daochulog.append("导出数据说明:\n")
if self.daochupath.text():
path=self.daochupath.text()
else:
path=QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DownloadLocation)
self.daochulog.append(f"导出文件保存路径:{path}")
self.daochulog.append(f"已选择模块:{len(self.selected_module_values)}个")
if items:
self.daochulog.append(f"查询到用例:{len(items)}条")
current_time=datetime.now()
formatted_time=current_time.strftime("%Y-%m-%d %H_%M_%S")
excelname=f'{path}/导出用例{formatted_time}.xlsx' # 保存路径 当前时间的时、分结尾命名
self.muban_excel(excelname) # 新建Excel
self.down_excel(items, items2, excelname) # 写入用例
else:
self.daochulog.append(f"选中模块用例数为【0】,请重新选择模块!")
self.daochu.setEnabled(True) # 导出按钮恢复
logging.info("导出用例按钮")
2.3.2 模板新建
使用openpyxl,进行新建模板文件,共两个页面。
def muban_excel(self, exclname):
"""新建模板Excel,传入Excel保存地址+名称:C:/Users/feng/Downloads/导出用例2024-02-04 09_36_31.xlsx"""
workbook=Workbook() # 新建
sheet=workbook.active
sheet.title="用例"
# 创建单元格样式对象
fill=PatternFill(start_color='F0F0F0', end_color='F0F0F0', fill_type='solid') # 设置背景颜色(淡灰色)
font=Font(bold=True) # 设置字体加粗
alignment=Alignment(horizontal='center', vertical='center') # 设置水平和垂直居中对齐
cell_style=NamedStyle(name="custom_style") # 创建并定义样式
cell_style.fill=fill
cell_style.font=font
cell_style.alignment=alignment
workbook.add_named_style(cell_style) # 将样式添加到工作簿的styles集合中
# 创建红色字体样式
red_font=Font(color="FF0000") # 红色
sheet.column_dimensions['B'].width=30 # 列宽
sheet.column_dimensions['D'].width=30
sheet.column_dimensions['E'].width=30
sheet.column_dimensions['G'].width=30
headers=["用例编号", "用例标题", "前置条件", "步骤", "预期", "关键词", "所属模块", "所属模块ID",
"优先级", "用例类型", "备注", "修改日期"]
red_font_headers_indices=[headers.index('用例标题'), headers.index('步骤'), headers.index('所属模块'),
headers.index('所属模块ID')]
# 写入表头并设置特定列的字体颜色
for col, header in enumerate(headers, start=1): # 注意:openpyxl中的列索引从1开始
cell=sheet.cell(row=1, column=col) # 表头在第一行
cell.value=header
cell.style=cell_style
if col - 1 in red_font_headers_indices:
cell.font=red_font
items=["1", "菜单验证", "", "打开页面跳转", "跳转正确", "菜单", "web功能测试用例", "12", "冒烟测试", "功能测试",
"备注", "2024-01-17 15:46:50"] # 示例数据
priority_dropdown_values=['冒烟测试', '高', '中', '低'] # 设置优先级和用例类型的下拉列表
case_type_dropdown_values=['功能测试', '性能测试', '配置相关', '安装部署', '接口测试', '安全相关', '兼容性测试',
'UI测试', '其它']
priority_col_index=headers.index('优先级') + 1 # 优先级列索引
# 创建优先级的数据验证对象
priority_data_validation=DataValidation(type="list", formula1='"{}"'.format(','.join(priority_dropdown_values)),
allow_blank=True)
sheet.add_data_validation(priority_data_validation)
for row in range(2, 100): # 从第二行开始应用(第一行为表头)
priority_data_validation.add(sheet.cell(row=row, column=priority_col_index))
case_type_col_index=headers.index('用例类型') + 1 # 同理为用例类型列设置下拉列表
case_type_data_validation=DataValidation(type="list",
formula1='"{}"'.format(','.join(case_type_dropdown_values)),
allow_blank=True)
sheet.add_data_validation(case_type_data_validation)
for row in range(2, 100):
case_type_data_validation.add(sheet.cell(row=row, column=case_type_col_index))
for col, v in enumerate(items, start=1): # 写入示例内容
cell=sheet.cell(row=2, column=col)
cell.value=v
headers2=["模块编号", "父模块id", "层级", "节点路径", "名称"]
items2=['1', '0', '0', '1',
'不能导入模块,请手动新增模块'] # 示例数据
module_sheet=workbook.create_sheet(title="模块") # 模块
module_sheet.column_dimensions['E'].width=45
for col, header in enumerate(headers2, start=1):
cell=module_sheet.cell(row=1, column=col) # 表头在第一行
cell.value=header
cell.style=cell_style
hang_number=2 # 初始化行数为2(从第二行开始写入)
for col, value in enumerate(items2): # 写入示例内容
cell=module_sheet.cell(row=hang_number, column=col + 1) # Excel索引是从1开始的,所以这里用col+1
cell.value=value
hang_number+=1 # 行数增加
workbook.save(f'{exclname}') # 保存
对必填字段标红,优先级、用例类型设置下拉项。同时填充示例一条
2.3.3 导出用例写入
打开新建的模板,分别写入用例、模块数据。
def down_excel(self, items, items2, excelname):
"""打开Excel地址+名称excelname。写入用例items、模块items2。"""
try:
workbook=load_workbook(excelname)
sheet=workbook["用例"]
hang_number=2 # 初始化行数为2(从第二行开始写入)
for item in items:
for col, value in enumerate(item):
cell=sheet.cell(row=hang_number, column=col + 1) # Excel索引是从1开始的,所以这里用col+1
cell.value=value
hang_number+=1 # 行数增加
module_sheet=workbook["模块"]
hang_number2=2 # 初始化行数为2(从第二行开始写入)
for item2 in items2:
for col, value in enumerate(item2):
cell=module_sheet.cell(row=hang_number2, column=col + 1) # Excel索引是从1开始的,所以这里用col+1
cell.value=value
hang_number2+=1 # 行数增加
workbook.save(f'{excelname}') # 保存
self.daochulog.append(
f"\n-----------------用例导出完成,共导出【{hang_number - 2}】条用例-----------------")
except Exception as e:
self.daochulog.append(f"出错了:{e}")
logging.error(e)
3、Excel导入用例
导入流程:选择导入文件,点击导入。
3.1 导入按钮
获取导入文件地址,导入按钮置灰,进行文件校验,校验通过读取数据,写入库表。完成后按钮恢复。
def upload(self):
"""读取导入Excel"""
self.daorulog.clear() # 清空说明
self.daorulog.append("导入数据说明:")
if self.daoruwenjian.text():
path=self.daoruwenjian.text() # 导入文件地址
else:
self.daorulog.append("请选择导入文件")
return
self.daochu_2.setEnabled(False) # 导入按钮置灰
self.daorulog.append(f"导入文件路径:{path}")
sheet=self.check_excl(path) # 校验Excel格式
if sheet:
self.save_excl(sheet) # 写入库表
self.daochu_2.setEnabled(True) # 导入按钮恢复
logging.info("导入Excel")
3.2 校验Excel
打开Excel后,对文件进行校验。读取“用例”sheet页面,没有读取到报错。
首先对行数、列数进行基本的验证。
然后对4列必填项读取,与页面最大行数进行比对,验证是否必填。
获取模块ID集合,查询出库内模块ID,进行对比。验证是否有效ID。
对用例优先级、类型,进行码值验证。
def check_excl(self,path):
"""校验Excel,输入文件地址。返回sheet"""
try:
wb=load_workbook(path) # openpyxl,打开Excel
sheet=wb["用例"]
self.daorulog.append("\n文件校验中...")
if sheet.max_row<=1:
self.daorulog.append("\t校验失败:未读取到用例数据") # 行列判断
return
elif sheet.max_column !=12:
self.daorulog.append("\t校验失败:导入文件列数有误")
return
# 文件内容校验
biaoti = [column.value for column in sheet['B'] if column.value is not None and column.value != ""]
buzou= [column.value for column in sheet['D'] if column.value is not None and column.value != ""]
mkname= [column.value for column in sheet['G'] if column.value is not None and column.value != ""]
mkid= [column.value for column in sheet['H'] if column.value is not None and column.value != ""]
required_columns=[biaoti, buzou, mkname, mkid] # 长度,判断必填
mkid_set=set(mkid[1::]) # 集合去重Excel数据,判断ID
if not all(len(col) == sheet.max_row for col in required_columns):
self.daorulog.append("\t校验失败:请检查必填项是否全部填写")
return
else:
self.case_db.connect()
moduleids=self.case_db.query_many("select moduleid from module where status = 10") # 库内有效模块ID
self.case_db.over()
moduleids=set(id for id_tuple in moduleids for id in id_tuple) #模块集合
difference=mkid_set.difference(moduleids) # 计算差集,判断模块ID
if difference:
self.daorulog.append(f"\t校验失败:有未识别的模块ID{difference}")
return
# 判断优先级、用例类型,码值
yxj= [column.value for column in sheet['I'] if column.value is not None and column.value != ""]
yllx= [column.value for column in sheet['J'] if column.value is not None and column.value != ""]
yxj_set=set(yxj[1::])
yllx_set=set(yllx[1::])
if yxj_set:
priority=tc_sql.codes_dict['priority'].keys() # 优先级码值判断
difference=yxj_set.difference(set(priority)) # 计算差集
if difference:
self.daorulog.append(f"\t校验失败:有未识别的优先级{difference}")
return
if yllx_set:
tc_types=tc_sql.codes_dict['tc_types'].keys() # 用例类型码值判断
difference=yllx_set.difference(set(tc_types)) # 计算差集
if difference:
self.daorulog.append(f"\t校验失败:有未识别的用例类型{difference}")
return
self.daorulog.append("\t校验通过")
return sheet
except:
self.daorulog.append(f"读取{path}失败,请检查文件与“用例”sheet\n")
3.3 写入库表
打开游标后,每次获取100行数据,读取每行进行数据转换,executemany一次插入多条语句。
所有的Excel数据插入完成后,再进行事务提交,有问题进行回滚。保证结果一致性,同时避免重复提交,增加数据库时间开销。
def save_excl(self,sheet):
"""读取导入Excel,写入到数据库"""
self.daorulog.append("写入数据中...")
row_start=2 # 起始行数
num=0 # 计数
current_time=datetime.now()
formatted_time=current_time.strftime("%Y-%m-%d %H:%M:%S")
self.case_db.connect()
try:
while row_start <= sheet.max_row:
batch_data=[] # 存储当前批次的用例数据
batch_rows=min(sheet.max_row - row_start + 1, 100) # 获取当前批次的行数(不超过剩余行数)
# 遍历当前批次的行
for row in sheet.iter_rows(min_row=row_start, max_row=row_start + batch_rows - 1):
data=[cell.value if cell.value is not None else '' for cell in row[1:11]] # B-K列
data[5]=10 # 添加用例状态
data[8]=tc_sql.codes_dict['tc_types'].get(data[8], '') if data[8] else '' # 码值转换
data[7]=tc_sql.codes_dict['priority'].get(data[7], '') if data[7] else ''
data.append(formatted_time) # 修改时间
num+=1 # 计数
batch_data.append(tuple(data))
if batch_data:
placeholders=','.join(['?'] * len(batch_data[0])) # 将批次数据转换为可执行的SQL语句
insert_sql=f'INSERT INTO testcase (title,preconditions,step,expect,keyword,status,moduleid,priority,types,remarks,modificationdate) VALUES ({placeholders})'
self.case_db.cursor.executemany(insert_sql, batch_data) # 批量插入数据
self.daorulog.append(f"\t已写入{num}条用例")
row_start+=batch_rows # 更新下一批的起始行
except:
self.case_db.conn.rollback() # 发生错误时回滚事务
self.daorulog.append("写入失败,数据已回滚")
else:
self.case_db.conn.commit() # 在所有数据成功插入后提交事务
self.daorulog.append(f"------------------导入完成,共写入:【{num}】条用例-----------------")
finally:
self.case_db.over()
3.4 导入实现情况
现在导入文件只对格式与部分内容做了校验,没有对模块名称做校验。数据已经可以成功导入了。
代码放在Gitee