【PC桌面自动化测试工具开发笔记】(三)使用ast库解析测试脚本自动提取测试数据

前言

前篇:【PC桌面自动化测试工具开发笔记】(二)数据分离可视窗口

自研Windows桌面自动化测试工具采用airtest批量自动化测试框架,一个.py文件为一个测试case。由于不采用测试类测试函数管理,常规的ddt数据驱动方法无法直接应用到项目上,偶然发现Airtest IDE用到了一个python语法解析库,便想到了用库解析python语法→提取变量→用户筛选变量→确认分离测试数据→保存测试数据到测试数据管理表→自动填充替换代码以实现自动数据分离数据驱动的目标。
以下是目标需求:

  1. 实现从测试数据管理表读取数据
  2. 实现从测试case python代码中自动提取变量并展示,由用户确认筛选
  3. 实现提取变量数据的本地保存,代码的自动替换填充

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代码的分离。


  1. Python语法解析参考文章 ↩︎

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值