(三)使用ast库解析测试脚本自动提取测试数据
前言
前篇:【PC桌面自动化测试工具开发笔记】(二)数据分离可视窗口
自研Windows桌面自动化测试工具采用airtest批量自动化测试框架,一个.py文件为一个测试case。由于不采用测试类测试函数管理,常规的ddt数据驱动方法无法直接应用到项目上,偶然发现Airtest IDE用到了一个python语法解析库,便想到了用库解析python语法→提取变量→用户筛选变量→确认分离测试数据→保存测试数据到测试数据管理表→自动填充替换代码以实现自动数据分离数据驱动的目标。
以下是目标需求:
- 实现从测试数据管理表读取数据
- 实现从测试case python代码中自动提取变量并展示,由用户确认筛选
- 实现提取变量数据的本地保存,代码的自动替换填充
Python语法解析的实现
参考csdn的文章,见文末链接1。
主要改进点:多元赋值变量的识别。
parse_function.py
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import ast
class ParseTree(ast.NodeVisitor):
visit_protected_assign = False
def __init__(self):
self.lib_dict = {} # 库字典
self.var_dict = {} # 变量字典
self.func_dict = {} # 函数字典
self.class_dict = {} # 类字典
def generic_visit(self, node):
ast.NodeVisitor.generic_visit(self, node)
def visit_ClassDef(self, node):
self.class_dict[node.name] = [node.lineno]
ast.NodeVisitor.generic_visit(self, node)
def visit_ImportFrom(self, node):
self.lib_dict[node.module] = [node.lineno]
ast.NodeVisitor.generic_visit(self, node)
def visit_FunctionDef(self, node):
self.func_dict[node.name] = [node.lineno]
def visit_Assign(self, node):
if isinstance(node.targets[0], ast.Name):
assign_name = node.targets[0].id
assign_value = node.value.value
if self.visit_protected_assign or not assign_name.startswith("_"):
self.var_dict[assign_name] = (assign_value, node.lineno)
elif isinstance(node.targets[0], ast.Tuple):
for i, elt in enumerate(node.targets[0].elts):
assign_name = elt.id
assign_value = node.value.elts[i].value
if self.visit_protected_assign or not assign_name.startswith("_"):
self.var_dict[assign_name] = (assign_value, node.lineno)
class MyParse:
def __init__(self, file):
with open(file, encoding='utf-8') as f:
s = f.read()
self.py_root = ast.parse(s)
self.parse_tree = ParseTree()
self.parse_tree.visit(self.py_root)
def dump_tree(self):
print(ast.dump(self.py_root, indent=4))
def get_lib_dict(self):
return self.parse_tree.lib_dict
def get_class_dict(self):
return self.parse_tree.class_dict
def get_func_dict(self):
return self.parse_tree.func_dict
def get_var_dict(self):
return self.parse_tree.var_dict
if __name__ == '__main__':
py = r'D:\AirTest\Airtest_Runner\test0.py' # 读取py文件
py_root = MyParse(py)
print('库', py_root.get_lib_dict())
print('类', py_root.get_class_dict())
print('函数', py_root.get_func_dict())
print('变量', py_root.get_var_dict())
MyParse语法解析类就封装完成了,在实例化时传入测试case的脚本路径,用get_var_dict()即可获取.py文件中的全部变量。
提取变量界面实现
在前篇中已经用DataTable类实现子表展示,详见【PC桌面自动化测试工具开发笔记】(二)数据分离可视窗口。
缺少保存功能,为了偷懒少改动一些代码,将保存功能触发添加到QTableWidget 的关闭事件中,仅当关闭自动提取变量窗口时会发送一个close_signal信号,后续数据保存槽函数绑定该信号。
class DataTable(QTableWidget):
update_signal = pyqtSignal()
data_target = ""
close_signal = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("变量提取")
self.horizontalHeader().setStretchLastSection(True)
self.verticalHeader().setVisible(False)
self.setColumnCount(2)
self.setRowCount(0)
self.insertRow(self.rowCount())
self.setHorizontalHeaderLabels(['变量名', '值'])
self.setStyleSheet("QTableWidget{border: none;}")
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.resizeColumnsToContents()
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.tableWidget_context)
def closeEvent(self, event):
mbox = win32api.MessageBox(0, f"保存数据分离结果至{self.data_target}?", "警告", win32con.MB_OKCANCEL)
if mbox == 1:
self.close_signal.emit()
super().closeEvent(event)
def tableWidget_context(self, position: QPoint):
"""右键菜单"""
pop_menu = QMenu()
create_act = QAction("添加数据", self)
delete_act = QAction("删除数据", self)
pop_menu.addAction(create_act)
pop_menu.addAction(delete_act)
create_act.triggered.connect(lambda: self.insertRow(self.rowCount()))
create_act.triggered.connect(self.update_signal.emit)
delete_act.triggered.connect(self.delete_rows)
delete_act.triggered.connect(self.update_signal.emit)
pop_menu.exec_(self.mapToGlobal(position))
def delete_rows(self):
"""删除行数据"""
select_indexs = self.selectedIndexes()
if select_indexs:
selected_rows = sorted(set(i.row() for i in select_indexs), reverse=True) # 求出所选择的行数
for i in selected_rows:
self.removeRow(i) # 删除行
def input_data(self, row, assign, value):
"""
添加数据
:param row: 行
:param assign: 变量名
:param value: 值
:return:
"""
if row + 1 != self.rowCount():
self.insertRow(self.rowCount())
self.setItem(row, 0, QTableWidgetItem(str(assign)))
self.setItem(row, 1, QTableWidgetItem(str(value)))
self.resizeColumnsToContents()
def get_dict(self):
"""
以字典形式返回表数据
:return: Dict
"""
data_dict = {}
for row in range(self.rowCount()):
if self.item(row, 0) and self.item(row, 1):
value = self.item(row, 1).text()
try:
value = int(value)
except:
value = self.item(row, 1).text()
data_dict[self.item(row, 0).text()] = value
return data_dict
变量数据的本地保存与代码自动填充
我写的自研Windows桌面自动化测试工具是带有代码编辑界面的,代码自动填充的实现较为容易(相对于用外部编辑工具来说)。
在关闭自动提取变量窗口时由用户确认是否提取测试数据,若提取则将变量提取到测试数据管理表,代码自动填充到代码编辑界面中。
下面我的项目代码中截取的部分代码,完整实现需要自己放到对应的类中。
self.air_case_file_path为我的主窗口类存储测试case文件路径的类变量,self.textEdit_air_case为我的主窗口类代码编辑控件。
def split_assign(self):
"""提取变量"""
air_case_file_path = self.air_case_file_path
if os.path.isfile(air_case_file_path):
py_root = MyParse(air_case_file_path)
self.data_table = DataTable()
self.data_table.data_target = self.sender().text()
self.data_table.setWindowTitle(f"添加变量至{self.data_table.data_target}")
var_dict = py_root.get_var_dict()
for i, var in enumerate(var_dict.keys()):
self.data_table.input_data(i, var, var_dict[var][0])
self.data_table.setMaximumHeight(
self.data_table.verticalHeader().minimumSectionSize() * (
self.data_table.rowCount() + 1) + self.data_table.horizontalHeader().height())
def highlight_row(item):
assign_name = self.data_table.item(item.row(), 0).text()
num = var_dict[assign_name][1]
block = self.textEdit_air_case.document().findBlockByLineNumber(num - 1)
self.textEdit_air_case.setTextCursor(QTextCursor(block))
return assign_name, block
def save_data():
# 更新代码内容
self.textEdit_air_case.textCursor().beginEditBlock()
for j in range(self.data_table.rowCount()):
item = self.data_table.item(j, 0)
assign_name, block = highlight_row(item)
text_line = block.text()
search_value = text_line.find("=") # 以等号为分界线
value_part = text_line[search_value + 1:] # 赋值部分代码字符串
name_part = text_line[:search_value] # 变量部分代码字符串
name_list = name_part.split(',')
name_without_space_list = [name.replace(" ", "") for name in name_list]
index = name_without_space_list.index(assign_name)
value_list = value_part.split(',')
value_list[index] = f" input_data['{assign_name}']"
value_part = ','.join(value_list)
text_line = name_part + "=" + value_part
self.textEdit_air_case.moveCursor(QTextCursor.EndOfLine, QTextCursor.KeepAnchor)
self.textEdit_air_case.insertPlainText(text_line)
# 加入读取测试数据管理表代码
insert_line_num = var_dict[self.data_table.item(0, 0).text()][1]
block = self.textEdit_air_case.document().findBlockByLineNumber(insert_line_num - 1)
self.textEdit_air_case.setTextCursor(QTextCursor(block))
self.textEdit_air_case.insertPlainText("input_data = get_data(__file__)\n")
self.textEdit_air_case.textCursor().endEditBlock()
# 保存测试数据到测试数据管理表
file_path = os.path.join(data_path, self.data_table.data_target, TestDatas.data_manager)
excel_file = ExcelHandling(file_path)
title = air_case_file_path[air_case_file_path.rfind("\\") + 1:].translate(
str.maketrans('', '', digits)).replace(
".py", "")
row = excel_file.get_row_data("用例标题", title)
new_data_dict = self.data_table.get_dict()
if row:
data_dict = eval(row[0]["输入数据"])
if isinstance(data_dict, dict):
data_dict.update(new_data_dict)
excel_file.edit_row(row[1], [title, data_dict])
else:
excel_file.edit_row(row[1], [title, new_data_dict])
else:
excel_file.edit_row(len(excel_file.workbook_data) + 1, [title, new_data_dict])
print(f"数据已保存至{file_path}")
self.data_table.itemClicked.connect(highlight_row)
self.data_table.close_signal.connect(save_data)
self.data_table.show()
结果展示
将功能接入到代码编辑界面的右键菜单中,用户选择对应的测试数据名称。
提取变量界面展示效果:
确认提取变量后代码填充效果:
get_data()是我自己写的测试数据管理表读取函数,返回值为测试数据变量字典。
提取的变量数据可用前篇写到数据分离可视窗口查看、修改数据,再另存为测试数据管理表后即可生成多套测试数据,实现测试数据与测试case代码的分离。