演练: 操作Qt应用中的Table

针对Qt中的TableView/TableWidget组件的自动化;
本次演练使用的样例应用是dockwidgets;

背景

Qt应用中有时会使用到表格来展示/处理数据,这些表格是使用Qt的QTableView或者QTableWidget组件实现的,在CukeTest中统一识别作Table控件处理。提到表格数据就不得不提到围绕表格的一系列操作,以及与Excel、CSV甚至是数据库之间的交互操作。
本次演练便是针对围绕Qt应用中的表格进行的一系列操作自动化。

目标

了解如何使用CukeTest进行Qt表格的操作以及Qt表格与其它表格应用之间的交互操作,实现自动化处理。

  1. 读取xlsx表格文件中的数据;
  2. 读写应用中的表格;
  3. 将数据写入到xlsx文件中;
  4. 将数据写入到MySQL数据库中;
  5. 将数据写入到CSV文件中;

实际操作

用CukeTest可以方便的创建BDD(行为驱动)自动化测试脚本,因此我们在实际演练中会按照BDD的方式来开发脚本。

项目结构参考

在完成全部的编写后,项目的结构应该呈现如下:

├── features
│   ├── feature1.feature
│   ├── step_definitions
│   │   ├── definitions1.js
│   │   └── model.tmodel
│   └── support
│       ├── hooks.js
│       └── origin.xlsx
└── package.json

编写feature文件

按照“目标”一节中的自动化步骤,编写feature文件如下:

编写剧本文件

或者可以直接切换文本模式直接复制以下内容,二者是等效的:

# language: zh-CN
功能: Qt Table自动化
实现Qt TableView/TableWidget控件的自动化

  场景: 从xlsx文件中导入数据
    当读取"origin.xlsx"文件中的数据
    那么将数据写入到应用表格中

  场景: 从应用中导出数据到其它应用中
    当读取表格中全部数据
    那么将数据写入到xlsx文件"data.xlsx"中
    那么将数据写入到CSV文件"data.csv"场景: 单元格操作
    假如目标单元格在第90行第0列
    当修改数据为"New Value!"并验证
    那么滚动到目标单元格

更多剧本文件相关查阅剧本编辑概述

为模型识别控件

首先创建模型文件,接着通过“操作”->“样例应用”菜单启动演练所用的应用——dockwidget。
在本次自动化中,只需要添加Table对象就可以满足使用,但是为了方便复制代码节省时间,或者为了属性表格结构,可以添加几个单元格——TableItem对象。

编写脚本

接着是编写脚本,我们需要在步骤定义中编写实现步骤描述的脚本。

读写应用中的表格

步骤:读取表格中全部数据

读取表格数据非常简单,CukeTest针对Table控件也提供了data()方法,能够直接获取表格中的所有数据,并以二维数组的形式返回,返回结果形如:

JavaScript
[
    [ '100', 'EmilyIna', 'F', '77', 'NahumBing', 'ChaucerBeck' ],
    [ '101', 'NahumBing', 'F', '92', 'DanBeryl', 'BartonZora' ],
    [ '102', 'MarionQuintina', 'M', '90', 'TrollpoeCaesar', 'EmilyIna' ],
    ......
]

但是表格中不止有数据,表头也是同样重要的,CukeTest提供了columnHeaders()方法来获取表头的内容,返回一串一维数组。

JavaScript
[ '学号', '名字', '性别', '成绩', '父亲', '母亲' ]

因此,读取表格内容的步骤定义可以写作:

JavaScript
When("读取表格中全部数据", async function () {
    let header = await model.getTable('Table').columnHeaders();
    let data = await model.getTable("Table").data();
    // 用于在附件中格式化显示对象数据
    this.attach(JSON.stringify(data,null,'  '));
    this.attach(JSON.stringify(header, null, '  '));
    this.header = header;
    this.data = data;
});

最后两行代码用于传递数据。

步骤:将数据写入到应用表格中

接着是写入到应用中的表格,这里可以结合步骤读取{string}文件中的数据的定义来理解,因为此处写入到应用中的数据来自于xlsx文件。

CukeTest为Table控件提供了setCellValue()的方法,能够修改指定位置单元格的内容,并且提供了获取行列数量的rowCount()columnCount()方法,可以更方便的对表格进行遍历,当然此处由于xlsx中的数据结构跟应用中的表格一致,便没有使用这两个方法。步骤定义如下:

JavaScript
Then("将数据写入到应用表格中", async function () {
    let data = this.xlsxData;
    let headers = await model.getTable("Table").columnHeaders();
    for (let row = 0; row < data.length; row++) {
        let rowDatum = data[row];
        for (let col = 0; col < headers.length; col++) {
            let value = rowDatum[headers[col]];
            await model.getTable("Table").setCellValue(row, col, value);
        }
    }
});

由于this对象的只在同一个场景中共用,这个步骤调用的场景和上述的不是同一个,因此这里的表头值需要重新获取。

不同数据文件类型间的转换

考虑到应用中的表格数据有时候需要从别的文件中读取,也有可能需要写入为其它类型的文件,因此这一节我们来完成针对表格应用与xlsx文件以及csv文件之间的转换。

转换表格数据的辅助函数

已知应用中的表格读出来的数据是这个样子的:

JavaScript
data = [
    [ '100', 'EmilyIna', 'F', '77', 'NahumBing', 'ChaucerBeck' ],
     [ '101', 'NahumBing', 'F', '92', 'DanBeryl', 'BartonZora' ],
    [ '102', 'MarionQuintina', 'M', '90', 'TrollpoeCaesar', 'EmilyIna' ],
    ......
]
header = [ '学号', '名字', '性别', '成绩', '父亲', '母亲' ]

而读写csvxlsx文件的数据类型为对象数组,比如对于以上数据,要修改为以下形式:

至于为什么是这个格式,可以查看Excel读写库CSV读写的说明。

[
  {
    '学号': '100',
    '名字': 'EmilyIna',
    '性别': 'F',
    '成绩': '77',
    '父亲': 'NahumBing',
    '母亲': 'ChaucerBeck'
  },
  {
    '学号': '101',
    '名字': 'NahumBing',
    '性别': 'F',
    '成绩': '92',
    '父亲': 'DanBeryl',
    '母亲': 'BartonZora'
  },
  {
    '学号': '102',
    '名字': 'MarionQuintina',
    '性别': 'M',
    '成绩': '90',
    '父亲': 'TrollpoeCaesar',
    '母亲': 'EmilyIna'
  },
  ...
]

因此需要一个辅助函数将表头和数据分开的数据形式转换为后者这种对象数组的形式,代码如下:

JavaScript
function arrayToJson(data, header) {
    // 用于将二维数组转换成对象数组的方法
    // 用于构建写入xlsx和csv的数据
    let rows = data.map((datum) => {
        let keyvalueSet = {};
        datum.forEach((cell, index) => {
            keyvalueSet[header[index]] = cell;
        })
        return keyvalueSet;
    })
    return rows;
}

步骤:读取{string}文件中的数据

这一节需要读取support目录中的origin.xlsx数据文件,内容如下:

学号 名字 性别 成绩 父亲 母亲
001 TrollpoeCaesar F 100 ChaucerBeck NahumBing
002 HughesBill F 100 BartonZora DanBeryl
003 VioletPhoebe M 100 EmilyIna TrollpoeCaesar
004 BartonZora F 100 CrichtonSibyl FitzGeraldHamiltion

写入到xlsx文件中,如下:
origin.xlsx

步骤定义如下:

JavaScript
When("读取{string}文件中的数据", async function (xlsxName) {
    let workbook = xlsx.readFile("./features/support/" + xlsxName);
    let worksheetData = xlsx.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]);
    this.attach(JSON.stringify(worksheetData, null, '  '));
    this.xlsxData = worksheetData;
});

步骤:将数据写入到xlsx文件{string}中

这一节需要使用到CukeTest提供的Excel操作API,可以从工具箱中拖拽生成,关于Excel API的更多介绍可以查看文档Excel的读取。最后完成的代码如下:

JavaScript
Then("将数据写入到xlsx文件{string}中", async function (xlsxName) {
    let header = this.header;
    let data = this.data;
    let xlsxData = arrayToJson(data, header);
    let filename = "./features/support/" + xlsxName;
    let workbook = xlsx.readFile("./features/support/origin.xlsx");
    let sheet = xlsx.utils.json_to_sheet(xlsxData, { header: header });
    workbook.Sheets[workbook.SheetNames[0]] = sheet;
    xlsx.writeFile(workbook, filename);
});

由于xlsx文件的表格包含很多样式和宏信息,因此写入到xlsx文件的数据需要更多的处理,这里就直接读取项目底下现成的xlsx文件的数据作为模版。

步骤:将数据写入到CSV文件{string}中

接着是写入CSV文件,相比于xlsx文件,CSV文件是以纯文本格式储存数据的,因此写入也比较简单,直接将转换后的对象数组写入即可:

JavaScript
Then("将数据写入到CSV文件{string}中", async function (csvName) {
    let header = this.header;
    let data = this.data;
    let csvData = arrayToJson(data, header);
    this.attach(JSON.stringify(csvData, null, '  '));
    let filePath = "./features/support/" + csvName;
    Util.saveToCsvFile(csvData, filePath);
});

场景:操作单元格

上面的自动化中,操作的对象一直都是表格控件——Table自身,接下来要自动化的对象是Table的子控件——TableItem单元格控件。Table有一个方法getItem(row, col)可以返回目标位置的单元格对象。

注意,在所有方法中,带get-前缀的方法都是同步方法,调用时不需要加await

步骤:目标单元格在第{int}行第{int}列

取得目标单元格的自动化对象,并保存到this对象中传递。

JavaScript
Given("目标单元格在第{int}行第{int}列", async function (row, col) {
    let targetCell = model.getTable("Table").getItem(row, col);
    this.attach(await targetCell.value());
    this.cell = targetCell;
});

步骤:修改单元格数据并验证

单元格对象直接提供了set()方法可以修改单元格的值。修改后获取单元格的值并和预期的值比较进行断言验证。

JavaScript
When("修改数据为{string}并验证", async function (value) {
    let targetCell = this.cell;
    this.attach(await targetCell.value());
    await targetCell.set(value);
    let actualValue = await targetCell.value();
    assert.equal(actualValue, value, `修改后的值不为${value}`);
});

步骤:滚动到目标单元格

修改单元格的值后我们可以滚动到目标单元格处,方便进行查看。

JavaScript
Then("滚动到目标单元格", async function () {
    let targetCell = this.cell;
    await targetCell.scrollIntoView();
});

引入Hooks

在这里可以引入Hooks来完成以下操作:

  • 开始执行步骤前,启动被操作应用,这里是dockwidgets应用;并最小化CukeTest;
  • 每个场景执行完毕后使用delay()方法等待几秒方便观察现象;
  • 项目执行完毕后恢复CukeTest;(可选)并关闭被操作应用,不关闭可以方便观察应用运行结果。

Hooks的概念和用法可以查看Hooks钩子

JavaScript
const { CukeTest } = require('cuketest');
const { BeforeAll, After, AfterAll, setDefaultTimeout } = require('cucumber');
const { QtAuto } = require("leanpro.qt");
const {Util} = require('leanpro.common');
const path = require('path');

/// 超时时间和Hook设置 ///
setDefaultTimeout(30 * 1000);  //set step timeout to be 30 seconds
let proc;
BeforeAll(async function () {
    CukeTest.minimize();
    let extension = (process.platform === 'win32') ? '.exe' : '';
    proc = await QtAuto.launchQtProcessAsync(path.dirname(process.execPath) + "/bin/dockwidgets" + extension);
    await model.getApplication("dockwidgets").exists(10);
})

After(async function () {
    let screenshot = await model.getWindow("Dock_Widgets").takeScreenshot();
    this.attach(screenshot, 'image/png');
    await Util.delay(2000);
})

AfterAll(async function () {
    CukeTest.restore();
    CukeTest.maximize();
    Util.stopProcess(proc);
})

运行结果

运行结果

总结

以上就是针对Qt中的表格控件的自动化,针对表格的自动化场景难点就在与数据处理上,在CukeTest提供的操作API加持下,桌面应用的数据可以轻松的提取出来,应用与自动化测试或者RPA(机器人流程自动化)中。如果数据处理过程让你觉得很吃力,也可以尝试引入一些数据处理库。

results matching ""

    No results matching ""