博客

跨平台Qt桌面应用的远程自动化测试实践

cover

随着软件开发日趋复杂,许多桌面应用需要跨越 Windows、Linux 等多个操作系统提供一致的用户体验。如何在 Windows、Linux 等异构环境中高效地验证应用功能的一致性和稳定性,成为自动化测试流程中一个亟待解决的课题。传统的测试方法往往因环境隔离、界面差异和远程操作的复杂性而显得力不从心。

本文将通过一个具体的测试场景——使用开源文件传输工具 NitroShare——探讨如何构建一套可靠的远程自动化测试方案。我们将展示如何从一台 Windows 控制端,远程驱动一台 Linux 设备上的 Qt 桌面应用,并完成跨系统的功能验证,为处理类似场景提供一个可行的技术思路。

远程桌面自动化的主要挑战

在规划远程桌面自动化测试时,我们通常会遇到以下几个技术难点:

  • 环境隔离与通信: 测试脚本运行于本地(如 Windows),而被测应用(AUT)部署在远程服务器(如 Linux),两者需要建立稳定可靠的通信链路。
  • UI 控件的异构性: 同一款应用在不同操作系统下的 UI 控件实现、属性和行为可能存在显著差异,这对控件识别的兼容性提出了高要求。
  • 操作与验证的异步性: 在远程设备上执行一个操作后,其结果可能需要在本地设备上进行验证,这要求测试框架能够处理跨系统的长链路验证逻辑。
  • 识别技术的稳定性: 仅依赖坐标或图像的识别方式,在面对不同分辨率或主题时,鲁棒性较差。结构化识别(如基于控件属性)是保障稳定性的关键。

要有效应对这些挑战,一个理想的测试方案应具备远程执行、跨平台控件识别以及统一的脚本与报告管理能力。

构建跨平台测试:以 NitroShare 为例

NitroShare 是一款基于 Qt 开发的开源局域网文件传输工具,支持 Windows 和 Linux,适合作为跨平台测试的案例。

测试目标与环境准备

为验证跨平台远程测试的可行性,我们设计了如下测试场景:

  • 测试目标: 验证从远程 Linux 设备通过 NitroShare 发送文件到本地 Windows 设备的功能是否成功,并确认文件接收的正确性。
  • 测试环境:
    • 控制端 (本地): Windows 系统,安装 NitroShare 和测试工具 CukeTest,用于发起和控制测试流程。
    • 被测端 (远程): Linux 系统 (本文为 Ubuntu),安装 NitroShare 和 CukeTest(作为远程执行端)。
    • 网络要求: 两台设备需在同一局域网内,以确保 CukeTest Worker 可以被本地端连接。

在 Windows 上启动 NitroShare

流程设计与实现

整个测试流程可以通过录制与编写脚本相结合的方式完成。

阶段一:本地(Windows)环境准备与应用启动

首先,在 Windows 上启动 NitroShare 并打开传输历史窗口,为接收文件做准备。

操作步骤:

  1. 启动 NitroShare 应用。
  2. 应用启动后会最小化至系统托盘,需要右键点击其图标。
  3. 在菜单中选择 “View Transfers” 打开主窗口。

CukeTest 支持对 Qt 应用和原生 Windows 控件的混合自动化。我们可以通过录制功能自动生成操作脚本。由于 NitroShare 启动后主界面默认隐藏,录制过程需要先操作应用本身(Qt),再切换到系统托盘(Windows)。

录制设置

录制后生成的脚本片段如下,它记录了启动应用并从托盘打开窗口的过程:

const { RunSettings } = require("leanpro.common");
const { QtAuto } = require("leanpro.qt");
const { WinAuto } = require("leanpro.win");
(async () => {
    await RunSettings.set({reportSteps: true});
    let modelQt = QtAuto.loadModel(__dirname + "/recording.tmodel");
    let modelWin = WinAuto.loadModel(__dirname + "/recording.tmodel");

    // 启动本地NitroShare进程
    await QtAuto.launchQtProcessAsync("C:\\Program Files\\NitroShare\\nitroshare.exe");
    await modelQt.getApplication("nitroshare").exists(10);

    // 操作 Windows 系统托盘,打开应用主界面
    await modelWin.getButton("通知_V_形").click();
    await modelWin.getButton("NitroShare1").click(15, 13, 2); // 使用右键点击
    await modelQt.getMenuItem("View_Transfers...").click(51, 21);
})();

阶段二:远程(Linux)文件发送操作

接下来,我们需要连接到远程 Linux 设备,控制其上的 NitroShare 应用发送一个文件。

操作步骤:

  1. 启动远程 Linux 上的 NitroShare。
  2. 点击“Send Files”按钮。
  3. 在文件选择对话框中,指定要发送的文件。
  4. 在设备列表中选择本地 Windows 设备,并确认发送。

在执行此阶段脚本前,需先在远程 Linux 终端运行 cuketest worker 命令启动服务。该服务会提供一个 WebSocket 地址,用于本地 CukeTest 建立连接。

启动Worker服务

在本地录制时,配置好远程应用的路径,并启用“远程录制”功能,输入 Worker 地址即可。CukeTest 会将你在远程桌面上的操作自动翻译为脚本代码。

远程录制设置

对于 Linux 系统下某些无法通过标准控件识别的托盘图标,可以借助 CukeTest 的图像识别能力作为补充。通过“添加图案对象”功能,将图标截图保存为识别模型,脚本即可通过图像点击来完成操作,增强了脚本的适应性。

添加图案对象

补全后的远程操作脚本片段如下:

const { CukeTest } = require("cuketest");
(async () => {
    // 连接到远程Linux设备上的Worker服务
    let auto = await CukeTest.connect({
        wsEndpoint: 'ws://<Your-Linux-IP>:3131/ws'
    });
    const { runSettings: RunSettings } = auto;
    const { qtAuto: QtAuto } = auto;
    await RunSettings.set({reportSteps: true});
    let remoteModel = await QtAuto.loadModel(__dirname + "/recording_1.tmodel");

    // 远程启动NitroShare
    await QtAuto.launchQtProcessAsync("/usr/bin/nitroshare");

    // 注意:某些Linux桌面环境可能需要通过图像识别点击托盘图标
    await remoteModel.getPattern("pattern_nitroshare_icon").click();
    await remoteModel.getPattern("pattern_view_transfers_menu").click();

    // 执行文件发送操作
    await remoteModel.getButton("sendFilesBtn").click();
    await remoteModel.getEdit("fileNameEdit").set("/home/user/Documents/testfile.txt");
    await remoteModel.getButton("Open").click();
    await remoteModel.getTableItem("Windows_Host").click(); // 假设Windows主机名为"Windows_Host"
    await remoteModel.getButton("OK").click();

    // 操作完成,关闭远程连接
    auto.close();
})();

说明: 在 Linux 不同桌面环境中,系统托盘的实现差异较大,直接进行控件识别可能不稳定。此时,可以利用 CukeTest 的图像识别能力作为补充,将托盘图标和菜单项添加为图像对象,增强脚本的通用性。

阶段三:回到本地端 — 验证结果与断言

文件发送后,我们需要在本地 Windows 端的 NitroShare 窗口中验证传输状态是否为“成功”。

文件传输成功提示

验证点:

  1. 本地 NitroShare 传输列表中显示“Succeeded”状态。
  2. 本地目标文件夹中存在与发送文件一致的文件。

CukeTest 提供了检查点(Checkpoint)方法,可以方便地对控件属性和文件系统进行验证。

例如,使用 checkProperty() 方法可以断言控件上的传输状态值是否正确,这比检查界面截图更为可靠和高效。

// 验证本地 NitroShare 接收状态
await modelQt.getTableItem("Succeeded").checkProperty("data", "Succeeded");

同时,可以结合 Node.js 的 fs 模块来验证文件是否真实存在于本地文件系统中,形成完整的闭环验证。

阶段四:测试收尾

测试结束后,一个规范的流程应当包含环境清理步骤,即关闭所有由测试启动的应用程序,以确保下次测试的独立性。

可以通过调用应用对象自身的 quit() 方法,或 Util.stopProcess() 方法强制结束进程。

// 方式一:通过 quit() 方法关闭应用
await modelQt.getApplication("nitroshare").quit();

// 方式二:通过进程ID关闭应用(需在启动时保存进程ID)
let process = await QtAuto.launchQtProcessAsync("...");
await Util.stopProcess(process);

行为驱动 (BDD) 的测试用例整合

在完成各个操作模块的脚本录制后,为了提高测试用例的可读性和可维护性,我们通常会将其整合到一个结构化的测试框架中。这里,我们采用行为驱动开发(BDD)的模式,使用 Gherkin 语法来描述测试场景。

1. 使用 Gherkin 定义测试场景

首先,在 feature 文件中用自然语言描述整个测试流程。这种方式让技术人员和非技术人员都能清晰地理解测试的目的和步骤。

# language: zh-CN
功能: 跨平台桌面应用的远程自动化测试实践
  验证NitroShare跨平台自动化操作

  场景: 用户点击退出菜单项后应用关闭并显示退出状态
    假如启动Windows端NitroShare应用
    当远程连接启动Linux端NitroShare应用并发送文件
    而且验证Windows端文件传输成功
    那么退出应用

2. 实现测试步骤定义

接下来,我们将 Gherkin 中的每一个步骤与先前录制的自动化脚本代码进行绑定。CukeTest 可以为这些步骤自动生成函数框架,我们只需将对应的代码逻辑填充进去。

在整合过程中,可以对脚本进行优化,例如:

  • 模型复用: 由于 NitroShare 在 Windows 和 Linux 平台上的 Qt 控件属性基本一致,可以共用同一个UI模型文件,简化维护。
  • 代码精简: 移除录制过程中产生的非必要参数,让代码更具通用性。
  • 增加断言: 在关键节点加入截图或日志,丰富测试报告内容,便于问题追溯。

整合后的测试脚本代码如下:

const { Given, When, Then, setDefaultTimeout } = require('cucumber');
const { CukeTest } = require("cuketest");
const { QtAuto } = require("leanpro.qt");
const { WinAuto } = require('leanpro.win');
const { Util } = require('leanpro.common');

// 为异步操作设置合理的超时时间
setDefaultTimeout(60 * 1000); 

// 加载用于本地自动化的UI模型文件
let localWinModel = WinAuto.loadModel(__dirname + "\\model1.tmodel");
let localQtModel = QtAuto.loadModel(__dirname + "\\model1.tmodel");

Given("启动Windows端NitroShare应用", async function () {
    // 启动本地应用并打开传输界面
    localAppProcessId = await QtAuto.launchQtProcessAsync("C:\\Program Files\\NitroShare\\nitroshare.exe");
    await localWinModel.getButton("通知_V_形").click()
    await localWinModel.getButton("NitroShare").click(0, 0, 2);
    await localQtModel.getMenuItem("View_Transfers...").click();

    // 截图并添加到报告中
    screenshot = await localQtModel.getWindow("TransferWindow").takeScreenshot()
    this.attach(screenshot, 'image/png')
});

When("远程连接启动Linux端NitroShare应用并发送文件", async function () {
    let auto = await CukeTest.connect({
        wsEndpoint: 'ws://10.0.2.15:3131/ws'
    });
    const { runSettings: RunSettings } = auto;
    const { qtAuto: QtAuto } = auto;
    await RunSettings.set({ reportSteps: true });

    //启动Qt应用文件"nitroshare"
    await QtAuto.launchQtProcessAsync("/usr/bin/nitroshare");
    let remoteQtModel = await QtAuto.loadModel(__dirname + "\\model1.tmodel");

    // 通过图像打开文件传输界面
    await remoteQtModel.getPattern("pattern_nitroshare").click()
    await remoteQtModel.getPattern("pattern_view_transfers").click()

    let screenshot = await remoteQtModel.getWindow("TransferWindow").takeScreenshot()
    this.attach(screenshot, 'image/png')

    await remoteQtModel.getButton("sendFilesBtn").click();
    await remoteQtModel.getEdit("fileNameEdit").set("/home/leanpro/Downloads/hello.txt")

    screenshot = await remoteQtModel.getWindow("QFileDialog").takeScreenshot()
    this.attach(screenshot, 'image/png')

    await remoteQtModel.getButton("Open").click();
    await remoteQtModel.getTableItem("Windows").click()

    screenshot = await remoteQtModel.getWindow("DeviceDialog").takeScreenshot()
    this.attach(screenshot, 'image/png')

    await remoteQtModel.getButton("OK").click();

    auto.close();
});


When("验证Windows端文件传输成功", async function () {
    // 检查本地UI,确认接收状态为 "Succeeded"
    await qtModel.getDataItem("Succeeded").checkProperty("data", "Succeeded");
    let screenshot = await localQtModel.getWindow("TransferWindow").takeScreenshot()
    this.attach(screenshot, 'image/png')

    // 此处还可结合文件系统API,验证物理文件是否存在
});

Then("退出应用", async function () {
    // 关闭本地 NitroShare
    await localQtModel.getApplication("nitroshare").quit()

    // 连接远程 Worker 并关闭远程 NitroShare
    let auto = await CukeTest.connect({
        wsEndpoint: 'ws://10.0.2.15:3131/ws'
    });
    const { qtAuto: QtAuto } = auto;
    let remoteQtModel = await QtAuto.loadModel(__dirname + "\\model1.tmodel");
    await remoteQtModel.getApplication("nitroshare").quit()
    await auto.close();
});

执行测试并生成报告

完成脚本整合后,直接运行该项目。CukeTest将自动执行整个流程:连接远程设备、操作UI、执行验证,并最终生成一份包含详细步骤、截图和执行结果的可视化报告。

在CukeTest中运行测试项目

运行报告

结语

本文通过一个具体的跨平台桌面应用测试案例,完整展示了如何构建一个从远程操作到本地验证的闭环自动化流程。这个过程看似复杂,但通过合适的工具和方法论,可以被拆解为一系列清晰、可管理和可自动化的步骤。

这表明,借助合适的工具链,团队完全有能力应对多平台桌面应用的测试挑战。将自动化测试与 BDD 等方法结合,不仅能提高测试执行的效率和一致性,还能增强测试用例的透明度和可维护性,为保障复杂应用的功能质量提供了可靠的技术路径。