本次计划的核心任务是开发一个,个人版的公共用例库,旨在将各系统和各类测试场景下的通用、基础以及关键功能的测试用例进行系统性地归纳整理,并以提高用例的复用率为目标,力求最大限度地减少重复劳动,提升测试效率。
计划内容:完成公共用例库的开发实施工作,包括需求分析、系统设计、开发、测试、打包、运行维护等工作。
需求分析、数据库表的设计:公共用例库计划–个人版(一)
主体界面与逻辑设计:公共用例库计划–个人版(二)
导出Excel功能:公共用例库计划–个人版(三)
模块选择功能改造与性能优化公共用例库计划–个人版(四)
QtCharts制作首页饼图与柱状图公共用例库计划–个人版(五)
典型Bug:新增遇见的较典型的bug,进行复盘与经验总结。
1. bug、图片的库表设计
2. 典型Bug页面,界面设计
3. 逻辑代码开发
计划创建两张表,一张表存放bug信息,一张表保存bug的图片。
建表语句:
# 创建bug表
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS bug (
bugid INTEGER PRIMARY KEY AUTOINCREMENT, --编号
title TEXT NOT NULL, --标题
types INTEGER , --类型--对应码值表code40-49
difficulty INTEGER , --难度--对应码值表code30-36
moduleid INTEGER , --所属模块
describe TEXT , --描述
think TEXT , --思考
severity INTEGER --严重程度--对应码值表code50-53
)""")
# 创建图片表
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS image (
imageid INTEGER PRIMARY KEY AUTOINCREMENT, --编号
name TEXT NOT NULL, --名称
image_bytes BLOB , --图片
bugid INTEGER --所属bug
)""")
主要功能设计介绍:
左侧使用listWidget显示bug列表,有翻页功能。
主体上面,把用例页面的查询条件copy过来改改,选择条件对左边列表的bug进行查询。
主体中部,点击左侧列表中bug名称,显示对应bug的详细信息。有图片上传按钮,显示图片、翻页等功能按钮。
最后建了一个窗口,放一个graphicsView控件,显示大图。(上面页面图片是label显示的,担心显示太小了)
在设计页面时,对保存按钮增加信号槽,点击保存触发save_bug。
对标题、描述进行 必填验证。如果有获取到bugid,就走编辑流程,没有就新增bug。
def save_bug(self):
"""bug页面,保存"""
if not self.lineEdit_4.text() or self.textEdit.toPlainText() == '[步骤]\n\n[结果]\n\n[期望]\n':
self.ts.xinxi("标题或描述不能为空!")
return
self.save_bug_b.setEnabled(False) # 保存按钮置灰
parts=self.mk_2.currentText().split("—ID:") # 模块编号
if len(parts) == 2:
self.mk_2.mkid=parts[1]
else:
self.mk_2.mkid=''
if self.bugid != 0: # 编辑
vlue=(f'{self.lineEdit_4.text()}',
tc_sql.codes_dict['mk_types'][f'{self.lileixing_bug_2.currentText()}'],
tc_sql.codes_dict['difficulty'][f'{self.nandu_2.currentText()}'],
f'{self.mk_2.mkid}', f'{self.textEdit.toPlainText()}',
f'{self.textEdit_2.toPlainText()}',
tc_sql.codes_dict['severity'][f'{self.nandu_3.currentText()}'], f'{self.bugid}')
self.savesql_bug(
"update bug set title=? ,types=? ,difficulty=?, moduleid=? ,describe = ?,think = ?,severity=? where bugid=?",
vlue)
else: # 新增
vlue=(f'{self.lineEdit_4.text()}',
tc_sql.codes_dict['mk_types'][f'{self.lileixing_bug_2.currentText()}'],
tc_sql.codes_dict['difficulty'][f'{self.nandu_2.currentText()}'],
f'{self.mk_2.mkid}', f'{self.textEdit.toPlainText()}',
f'{self.textEdit_2.toPlainText()}',
tc_sql.codes_dict['severity'][f'{self.nandu_3.currentText()}'])
self.savesql_bug("INSERT INTO bug VALUES (NULL,?,?,?,?,?,?,?)", vlue)
def savesql_bug(self, sql, vlue):
"""bug保存sql"""
self.case_db.connect()
if self.case_db.operate_one(sql, vlue):
self.case_db.over()
self.chaxun_bug() # 查询列表
self.ts.xinxi("保存成功")
logging.info('bug页面,保存')
else:
self.ts.xinxi("保存失败,请检查")
self.save_bug_b.setEnabled(True) # 保存按钮恢复
在设计页面时,对每一个查询条件设计信号槽,触发查询按钮,查询按钮触发chaxun_bug。
先获取查询条件的信息,然后通过拼接条件sql,查询出对应的bug,写入bug列表显示。
def chaxun_bug(self, ye=1):
"""bug页面,查询"""
parts=self.mk_1.currentText().split("—ID:")
if len(parts) == 2:
self.mk_1.mkid=parts[1]
else:
self.mk_1.mkid=''
mk_types=tc_sql.codes_dict['mk_types'][
f'{self.lileixing_bug.currentText()}'] if self.lileixing_bug.currentText() else ''
difficulty=tc_sql.codes_dict['difficulty'][
self.nandu.currentText()] if self.nandu.currentText() else ''
dic={"bug.types": mk_types,
"bug.difficulty": difficulty,
"bug.moduleid": self.mk_1.mkid}
dic_like={"bug.title": self.biaoti_bug.text()}
sql1="select bugid || ':' || title from bug where" # 拼接sql查询条件
sql2="select count(*) from bug where"
for k, v in dic.items():
if v:
sql1+=' ' + k + "= " + str(v) + " and"
sql2+=' ' + k + "= " + str(v) + " and"
for k, v in dic_like.items():
if v:
sql1+=' ' + k + " like '%" + v + "%' and"
sql2+=' ' + k + " like '%" + v + "%' and"
offset=20 * (ye - 1) # 分页查询
sql1=sql1.rstrip('where').rstrip('and') + " order by bug.bugid DESC " + f"LIMIT 20 OFFSET {offset}"
sql2=sql2.rstrip('where').rstrip('and')
self.case_db.connect()
items=self.case_db.query_many(sql1) # bug查询
count=self.case_db.query_one(sql2)
self.bugye_count=math.ceil(count[0] / 20) # 页数计算
self.case_db.over()
self.listWidget.clear() # 清空列表
self.clear_bug() # 清空页面
if items:
for i in range(len(items)): # 数据写入列表
item=QListWidgetItem(items[i][0])
item.setToolTip(items[i][0]) # 写入提示
self.listWidget.addItem(item)
self.label_24.setText(f"总数: {count[0]}条 共: {self.bugye_count}页 ")
self.lineEdit_3.setText(f"{ye}")
else:
self.listWidget.addItem("未查询到数据") # 查询无数据,加空行
self.label_24.setText(f"总数: 0条 共: 0页 ")
self.lineEdit_3.setText("0")
logging.info('bug页面,查询')
查询出bug列表后,点击其中的bug,页面显示详情。对列表增加槽函数,点击触发show_bug。
获取点击的bugID,根据bugid查询出信息,回显到页面。
def show_bug(self):
"""点击bug,显示bug详情"""
a=self.listWidget.selectedItems()
if a:
parts=a[0].text().split(":")
if len(parts) == 2:
self.bugid=parts[0]
else:
self.bugid=0
return
sql26='''select bug.title,codes1.value as types,codes2.value as difficulty,
module.modulename || '—ID:' || module.moduleid,
bug.describe,bug.think,codes3.value as severity
from bug
INNER JOIN
codes AS codes1 ON bug.types = codes1.id AND codes1.id BETWEEN 40 AND 49 -- 关联类型码值表
INNER JOIN
codes AS codes2 ON bug.difficulty = codes2.id AND codes2.id BETWEEN 30 AND 36 -- 关联难度码值表
INNER JOIN
codes AS codes3 ON bug.severity = codes3.id AND codes3.id BETWEEN 50 AND 53
LEFT JOIN
module ON bug.moduleid = module.moduleid
where bug.bugid = ? '''
self.case_db.connect()
items=self.case_db.query_one(sql26, (self.bugid,)) # bug查询
self.case_db.over()
# bug详情回显
self.lineEdit_4.setText(items[0])
index=self.lileixing_bug_2.findText(items[1], Qt.MatchFlag.MatchFixedString)
self.lileixing_bug_2.setCurrentIndex(index) # 设置下拉框内容
index2=self.nandu_2.findText(items[2], Qt.MatchFlag.MatchFixedString)
self.nandu_2.setCurrentIndex(index2)
self.mk_2.clear() # 清空模块下拉
self.mk_2.addItem(items[3])
self.mk_2.setCurrentIndex(0) # 设置模块下拉显示
self.textEdit.setText(items[4])
self.textEdit_2.setText(items[5])
index3=self.nandu_3.findText(items[6], Qt.MatchFlag.MatchFixedString)
self.nandu_3.setCurrentIndex(index3)
logging.info('显示bug详情')
现在bug新增,查询修改没问题。
bug保存后,上传图片。有删除、图片翻页、查看大图功能
上传图片按钮,触发函数up_image。
对图片大小、格式限制,然后将图片转成Base64格式存入数据库。
def up_image(self):
"""上传bug图片"""
if self.bugid == 0:
self.ts.xinxi("未获取到bugID,请先选择或者保存bug")
return
MAX_IMAGE_SIZE=1.5 * 1024 * 1024 # 图片大小限制(例如:5MB)
try:
filename, _=QFileDialog.getOpenFileName(self, "选择图片", "",
"Image Files (*.jpg *.jpeg *.png *.bmp *.gif)")
if not filename: # 未选择图片,退出
return
name=filename.split('/')[-1]
file_size=os.path.getsize(filename) # 检查文件大小
if file_size > MAX_IMAGE_SIZE:
self.ts.xinxi(f'图片大小超过限制({MAX_IMAGE_SIZE / (1024 * 1024)}MB),请上传更小的图片!')
else:
img=Image.open(filename)
img.save(f"_internal/{name}", quality=90) # 压缩图片
with open(f"_internal/{name}", 'rb') as image_file:
content=base64.b64encode(image_file.read()) # 转换成Base64格式
os.remove(f"_internal/{name}")
self.case_db.connect()
sql=f"INSERT INTO main.image (name, image_bytes,bugid) VALUES (?, ?, ?);"
if self.case_db.operate_one(sql, (name, content, self.bugid)):
self.case_db.over()
self.show_image(self.bugid)
logging.info('上传bug图片')
else:
self.ts.xinxi("图片上传出错")
except Exception as e:
self.ts.xinxi(f"上传出错:{e}")
logging.error(e)
图片上传后,就触发显示函数。将查询bug的图片,显示到label
首先对图像显示区域进行清理,如果查询到图片,将显示图片、名称、页数。
def show_image(self, bugid, number=0):
"""根据bugID,查询bug图片"""
self.label_image.setText('暂无图片') # 清空bug图片区域数据
self.pushButton_10.setVisible(False) # 删除图片按钮,隐藏
self.label_15.clear()
self.label_34.clear()
self.imageid=None
self.case_db.connect()
sql=f"SELECT image_bytes,imageid,name FROM image WHERE bugid=? LIMIT 1 OFFSET {number} "
value=self.case_db.query_one(sql, (bugid,))
count=self.case_db.query_one(f"SELECT count(*) FROM image WHERE bugid=? ", (bugid,))
self.case_db.over()
if value: # 显示图片、数量、名称
self.imageid=value[1]
str_encode=base64.b64decode(value[0]) # base64编码对应的解码(解码完字符串)
pixmap=QPixmap()
pixmap.loadFromData(str_encode)
self.label_image.clear()
scaled_pixmap=pixmap.scaled(self.label_image.size(), Qt.AspectRatioMode.KeepAspectRatio)
self.label_image.setPixmap(scaled_pixmap)
self.label_15.setText(f"{number + 1}/{count[0]}") # 数量
self.label_34.setText(f"{value[2]}")
self.pushButton_10.setVisible(True) # 删除图片按钮,显示
logging.info('bug图片显示')
点击页面图片,触发大图窗口。
def image_clicked(self, event):
"""点击图片,打开图片窗口"""
if self.imageid:
self.case_db.connect()
sql=f"SELECT image_bytes FROM image WHERE imageid=?"
value=self.case_db.query_one(sql, (self.imageid,))
self.case_db.over()
str_encode=base64.b64decode(value[0])
if not self.child_windows:
image_max=Image_bug(str_encode)
self.child_windows.append(image_max) # 将新打开的窗口添加到子窗口列表
else:
self.child_windows[0].update_image(str_encode) # 已有窗口,更新图片
logging.info('bug图片,大图窗口')
大图窗口,继承界面设计好的窗口。
使用单例模式,只允许打开一个窗口,再次点击图片就更新窗口图片显示。
对鼠标滚轮进行重写,实现放大缩小图片。
class Image_bug(QWidget, Ui_imagemax):
"""图片页面"""
_instance=None
def __new__(cls, str_encode, parent=None):
if cls._instance is None:
instance=super().__new__(cls)
instance.__init__(str_encode, parent)
cls._instance=instance # 保存到类变量以便后续使用
return instance
else:
cls._instance.update_image(str_encode) # 更新现有实例的图像数据
return cls._instance
def __init__(self, str_encode, parent=None):
if self._instance: # 避免多次初始化同一个实例
return
super().__init__(parent)
self.str_encode=str_encode
self.setupUi(self)
self.init_image()
def init_image(self):
"""窗口加载图片显示"""
pixmap=QPixmap()
pixmap.loadFromData(self.str_encode)
scene=QGraphicsScene()# 创建图形视图、场景及图像项目
item=QGraphicsPixmapItem(pixmap)
scene.addItem(item)
self.graphicsView.setScene(scene)# 设置视图与场景
self.graphicsView.wheelEvent=self.wheelEvent_handler # 重写滚轮事件处理函数
self.show()
def update_image(self, new_str_encode):
"""更新窗口图片"""
self.str_encode=new_str_encode
self.init_image() # 重新加载图片数据
self.activateWindow() # 激活窗口
self.raise_() # 提升窗口至最前面
def wheelEvent_handler(self, event):
"""滚轮放大缩小图片"""
zoom_in_factor=1.25
zoom_out_factor=1 / zoom_in_factor
delta=event.angleDelta().y() / 120
if delta > 0:
factor=zoom_in_factor
elif delta < 0:
factor=zoom_out_factor
else:
return
# 获取当前鼠标位置在视图坐标系中的位置
mouse_pos_scene = self.graphicsView.mapToScene(QPoint(int(event.position().x()), int(event.position().y())))
# 记录原始视图中心点
old_center=self.graphicsView.viewport().rect().center()
# 缩放视图
self.graphicsView.scale(factor, factor)
# 计算新的视图中心点,并确保鼠标下的内容不变
new_center = self.graphicsView.mapFromScene(mouse_pos_scene).toPointF()
self.graphicsView.centerOn(new_center)