Squish 深入:架构、对象识别与实战要点



Squish 是 Qt 生态里的 GUI(Graphical User Interface,图形用户界面)自动化测试工具,原属德国 froglogic,2019 年被 Qt 公司收购,现在的正式名称是 Qt Squish。它解决的问题是:把「启动程序、点按钮、填表单、看结果」这一整套手工回归测试变成可重复执行的脚本。

一、和图像比对工具的根本区别

市面上的 UI 自动化工具大体分两派。SikuliX、Airtest 这类靠屏幕截图匹配,找到目标图片就点下去;Squish 走的是另一条路——对象级识别。

Squish 启动被测程序(AUT,Application Under Test,被测应用)时会往进程里注入 hook 库,借 Qt 的元对象系统(Meta-Object System)直接拿到运行时的 widget 树。脚本里操作的不是坐标,而是真实的 QObject 指针的代理:

table = waitForObject(":Address Book.contacts_QTableWidget")
test.compare(table.rowCount, 5)       # 直接读 Qt 属性
table.sortByColumn(1, Qt.AscendingOrder)   # 甚至能调 slot

带来的直接好处:换一套 QSS 皮肤、改分辨率、把按钮从左边挪到右边,脚本照样跑。截图派在这些改动下会全线失败。

代价是需要为每种 UI 工具包做专门的 hook 支持,所以 Squish 是按工具包授权的:Qt Widgets、QML(Qt Modeling Language)/ Quick、Java(Swing / SWT,Standard Widget Toolkit / JavaFX)、Web 浏览器、Windows 原生(MFC,Microsoft Foundation Classes / WinForms / WPF,Windows Presentation Foundation)、macOS、iOS、Android、Tk。

二、架构:三个进程

面试常问「Squish 是怎么控制被测程序的」,答案是三段式:

  • squishserver — 常驻服务,负责启动 AUT 并注入 hook,管理连接。默认监听 4322 端口
  • squishrunner — 执行测试脚本,把脚本里的操作翻译成命令发给 server
  • Squish IDE — 开发环境,含录制回放、Spy(运行时对象浏览器)、Object Map 编辑器、验证点编辑器

数据流是 squishrunner → squishserver → AUT 内的 hook。理解这一层的实际意义在于:AUT 可以跑在另一台机器甚至另一个操作系统上,只要那边起了 squishserver。这是 Squish 做跨平台矩阵测试的基础。

命令行跑用例(CI 里就是这么接的):

# 注册 AUT
squishserver --config addAUT addressbook D:\build\addressbook\release

# 后台起 server
Start-Process squishserver

# 跑整个 suite,输出 JUnit XML 给 CI 收集
squishrunner --testsuite suite_addressbook `
             --reportgen junit,results.xml `
             --exitCodeOnFail 2

三、对象识别:Real Name 与 Symbolic Name

这是 Squish 最核心也最容易写坏的部分。定位一个控件有两种写法。

Real Name——属性组合的字面量,直接写在脚本里:

clickButton(waitForObject(
    "{type='QPushButton' text='OK' visible='true' "
    "window={type='QMainWindow' windowTitle='Address Book'}}"))

Symbolic Name——以冒号开头,指向 Object Map 里的一条记录:

clickButton(waitForObject(":Address Book.OK_QPushButton"))

Object Map(objects.map)里存的是符号名到真实属性组合的映射:

:Address Book_QMainWindow  {type='QMainWindow' windowTitle='Address Book'}
:Address Book.OK_QPushButton  {type='QPushButton' text='OK' \
                               window=':Address Book_QMainWindow'}

实践上一律用 Symbolic Name。理由很实际:界面改版后按钮文字从 OK 变成「确定」,用符号名只需改 Object Map 里一行,所有引用它的脚本自动修好;写死 Real Name 就得满仓库搜索替换。

3.1 让识别更稳的几条经验

  • 让开发给控件设 objectName。这是最有效的一招。text 会随国际化变,objectName 不会。有了它,属性组合可以简化成 {type='QPushButton' objectName='btnOk'}
  • 避免依赖 occurrence 序号。{type='QLabel' occurrence='3'} 这种在界面加一个标签后就错位了
  • 用 Spy 而不是猜。IDE 的 Spy 能在程序运行时展开完整对象树,看到每个控件的真实属性和可用方法
  • 动态列表用属性过滤。表格行、列表项这类运行时生成的对象,用 waitForObjectItem 配合文本定位

四、waitForObject 与同步问题

这是 Squish 用例不稳定(flaky test,间歇性失败的测试)的头号来源,也是必问题。

findObject(":Dialog.OK_QPushButton")     # 立即查找,不存在则抛异常
waitForObject(":Dialog.OK_QPushButton")  # 轮询等待,默认超时 20 秒

界面上任何异步产生的东西——网络请求返回后才填充的表格、延迟弹出的对话框、动画结束才可点击的按钮——都必须用 waitForObject。用 findObject 会随机失败,因为脚本执行速度远快于界面响应。

更麻烦的情况是对象已存在但状态没就绪,这时候要等条件而不是等对象:

// 等表格加载完数据,而不是等表格出现
waitFor(lambda: findObject(":contacts_QTableWidget").rowCount > 0, 10000)

// 等按钮变成可点击
waitFor(lambda: findObject(":Dialog.OK_QPushButton").enabled, 5000)

绝对不要用 snooze() 硬等。它是固定睡眠,写短了在慢机器上失败,写长了整个 suite 被拖慢。只有在实在找不到可判断的条件时才用,并且要写注释说明原因。

五、验证点

Squish 的断言都在 test 命名空间下:

test.compare(table.rowCount, 5)                  # 相等断言
test.verify(button.enabled)                      # 布尔断言
test.xcompare(a, b)                              # 预期失败(已知 bug)
test.fail("不该走到这个分支")
test.log("当前用户: " + username)                 # 只记录,不影响结果

# 截图比对,用于图表、自绘控件这类没法读属性的场景
test.vpImage("chart_baseline")

# 整个对象的属性快照比对
test.vp("contacts_table_state")

要点是 test.compare 失败后脚本会继续执行,只是把这条标记为 FAIL。想让它立刻中断得自己抛异常或提前 return。这个行为和 Qt Test 的 QCOMPARE 恰好相反,容易踩。

六、脚本组织与可维护性

录制回放只适合摸索阶段。录出来的脚本是一长串平铺的点击,改一次界面就废。真正能长期维护的写法是分层。

底层封装成页面对象(Page Object Pattern,页面对象模式):

# shared/scripts/pages.py
class AddressBookPage:
    def new_contact(self, forename, surname, email):
        clickButton(waitForObject(":Address Book.New_QToolButton"))
        type(waitForObject(":Forename:_QLineEdit"), forename)
        type(waitForObject(":Surname:_QLineEdit"), surname)
        type(waitForObject(":Email:_QLineEdit"), email)
        clickButton(waitForObject(":Address Book - Unnamed.OK_QPushButton"))

    def row_count(self):
        return waitForObject(":Address Book.contacts_QTableWidget").rowCount

用例层只表达业务意图:

source(findFile("scripts", "pages.py"))

def main():
    startApplication("addressbook")
    page = AddressBookPage()

    page.new_contact("Jifang", "Guo", "guo@example.com")
    test.compare(page.row_count(), 1)

    page.new_contact("Alex", "Chen", "alex@example.com")
    test.compare(page.row_count(), 2)

配套的目录约定:shared/scripts/ 放公共函数,shared/testdata/ 放 CSV/TSV 测试数据,suite_xxx/tst_yyy/ 是单个用例。数据驱动直接读表:

for record in testData.dataset("contacts.tsv"):
    name  = testData.field(record, "name")
    email = testData.field(record, "email")
    page.new_contact(name, "", email)

七、常见坑

  • hook 注入失败:AUT 的 Qt 版本要和 Squish 编译时用的匹配。静态链接的 Qt 程序需要特殊处理(--enable-attach 或改用 Squish 提供的链接方案)
  • 脚本改了界面状态没复位:每个用例应该假设自己从干净状态开始。用 cleanup() 函数或 startApplication 重启来保证隔离
  • 中文输入:type() 对输入法有依赖,中文文本建议用 setText 直接赋值,或走剪贴板
  • CI(Continuous Integration,持续集成)里没有图形环境:Linux 下需要 Xvfb(X Virtual Framebuffer,X 虚拟帧缓冲)虚拟显示;Windows 下注意会话隔离,服务账户跑不出交互式桌面
  • 用例铺太多:Squish 用例的维护成本是单元测试的数倍。只覆盖关键路径,逻辑验证留给 Qt Test

术语表

简称英文全称中文与说明
AUTApplication Under Test被测应用。被 Squish 注入 hook 并驱动的目标程序
GUIGraphical User Interface图形用户界面
—Object Map对象映射表。objects.map 文件,存符号名到属性组合的映射
—Real Name真实名。控件属性组合的字面量写法,直观但脆弱
—Symbolic Name符号名。以冒号开头指向 Object Map 的引用,推荐写法
—Verification Point (VP)验证点。test.* 系列断言,含属性快照和截图比对
—Page Object Pattern页面对象模式。把界面操作封装成类方法,用例层只表达业务意图
—flaky test间歇性失败的测试。同一份代码时而通过时而失败,多因同步问题
CIContinuous Integration持续集成
XvfbX Virtual FramebufferX 虚拟帧缓冲。Linux 无显示器环境下跑 GUI 测试用
QMLQt Modeling LanguageQt 建模语言。声明式 UI 语言
SWTStandard Widget Toolkit标准控件工具包。Eclipse 的 Java GUI 库
MFCMicrosoft Foundation Classes微软基础类库。Windows 原生 C++ GUI 框架
WPFWindows Presentation FoundationWindows 呈现基础。微软的 XAML 界面框架
XMLeXtensible Markup Language可扩展标记语言。Squish 报告输出格式之一
TSV / CSVTab / Comma Separated Values制表符 / 逗号分隔值。数据驱动测试的测试数据格式

小结

Squish 的价值在于对象级识别带来的稳定性,而它的成本在于对象映射的维护。把 Object Map 管好、坚持用 Symbolic Name、所有等待走 waitForObject、脚本按 Page Object 分层——这四条做到了,Squish 用例就能长期活着,否则很快会变成没人敢动的负债。

posted @ 2026-09-14 17:36  起飞的乌龟  阅读(33)  评论(0)    收藏  举报