QGroundControl 仓库中的 Qt Quick Test 47 条测试规则全解:编写 tst_*.qml 的规范与实战
QGroundControl 仓库中的 Qt Quick Test 47 条测试规则全解:编写 tst_*.qml 的规范与实战
本篇技术指南围绕 qt-qml-test 技能的核心契约文档(tools/skills/qt-qml-test/references/qt-quick-test-rules.md)展开,系统讲解为 QML 组件生成 Qt Quick Test 单元测试(tst_*.qml)时必须遵循的 47 条规则——涵盖 import 与结构、单/嵌套组件、属性与信号、鼠标键盘事件、Window 与 Singleton 源码、指针处理器触发、无断言测试函数等全部维度。读完本文,你将掌握一套"可直接落地"的测试编写规范,并能在 QGroundControl 仓库自带的 Qt Quick Test 基础设施(QGCQmlQuickTests、CTest 集成、offscreen 运行模式)中立即实践。
1. 背景:规则从何而来、如何组织
qt-qml-test 是一个面向 Claude Code、GitHub Copilot 等 AI Agent 的技能(skill),用于为一个或多个 QML 组件生成 Qt Quick Test 单元测试文件。技能本体定义在 tools/skills/qt-qml-test/SKILL.md 中:SKILL.md 在相同分类标题下为每条规则维护一行索引,而完整的规范性文本、示例与理由(rationale)则统一存放在 qt-quick-test-rules.md 中,即本文的核心契约文档。
规则的使用方式是:对被测组件应用其中所有相关的规则("Apply every rule relevant to the component under test"),而不是机械地逐条套用。规则按主题分为 10 个类别:
| 类别 | 规则编号 |
|---|---|
| Imports & structure(导入与结构) | 1–2 |
| Single vs nested components(单组件 vs 嵌套组件) | 3–5 |
| Properties(属性) | 6–11 |
| Signals & SignalSpy(信号与信号间谍) | 12–22 |
| Mouse & key events(鼠标与键盘事件) | 23–29 |
| Conventions(约定) | 30–33 |
| Per-control specifics(控件专项) | 34–39 |
| Property dependencies(属性依赖) | 40 |
| Window and singleton sources(窗口与单例源码) | 41–42 |
| 其余专项规则 | 43–47 |
配套的模板变体(8 种可粘贴模板)见 qt-quick-test-template.md,按症状归类的问题模式见 qt-quick-test-pitfalls.md,控件级交互与信号模式见 qt-quick-test-controls.md,属性测试模式见 qt-quick-test-properties.md。
仓库佐证:QGroundControl 仓库中已存在一套真实的 Qt Quick Test 基础设施(test/UnitTestFramework/QmlTesting/),其中 QmlTesting.h 描述了完整工作流:QML 测试文件(
tst_*.qml)放在tests/目录 → 每个文件包含TestCase与测试函数 → 专用可执行程序QGCQmlQuickTests通过quick_test_main()在独立进程中运行 → 结果接入 CTest。本文将规则讲解与这套真实基础设施互相印证。
2. 导入与结构(规则 1–2)
规则 1:QtQuick 与 QtTest 模块不带版本号导入。 这是 Qt 6 的写法——带版本号的导入(如 import QtQuick 2.15)是 Qt 5 时代的遗留。仅当测试脚本代码确实按名称引用了对应标识符时,才追加 Qt 自带模块的导入:
import QtQuick.Controls——仅当测试代码本身按名称引用某个 Controls 标识符时(如Slider.SnapAlways、Dialog.Ok、Popup.CloseOnEscape、Button.AutoRepeat等)才需要。源码里使用了 Controls 类型,并不自动要求测试中导入:findChild返回的是泛型QObject引用,其属性是动态解析的,所以button.text、button.background.radius不需要import QtQuick.Controls就能访问。源码中的风格限定导入(QtQuick.Controls.Basic)只是把类型引入源码的作用域,不会带入测试脚本作用域。import QtQuick.Layouts——当测试代码读写Layout.fillWidth、Layout.preferredHeight等时才需要。
不要添加未被引用的框架导入。拿不准时宁可不加——缺少导入会在运行时以 ReferenceError 暴露,届时补上即可。
规则 2:为外层 Item 设置与受测组件尺寸匹配的 width 和 height。 模板中常见的 800×600 只是示意值,应选择能舒适容纳被测组件的尺寸。
3. 单组件 vs 嵌套组件(规则 3–5)
规则 3:测试单个组件时使用直接引用:
let mycomponent = createTemporaryObject(myComponent, root)
verify(!!mycomponent, "Component exists")
始终把外层 Item(即 root)作为父对象传入——createTemporaryObject 的默认父对象是 TestCase,而 TestCase 是 visible: false 的,子对象不会进入可视树,输入事件会被静默破坏。这是"点击测试失败"最常见的原因(详见 qt-quick-test-pitfalls.md 的"合成鼠标事件到达不了控件"一节)。
规则 4:测试嵌套组件时,测试函数以 let app = createTemporaryObject(myComponent, root) 开头,再通过父对象访问子对象:let object = findChild(app, objectName)。禁止使用空的 objectName 调用 findChild。
规则 5:findChild 之后必须验证:verify(!!object, "Object exists")。 这条连同规则 3 的 "Component exists",是少数允许自定义消息的断言形式之一(见规则 30)。
嵌套组件的典型形态(来自 qt-quick-test-template.md 的 Variant 2):
function test_emailFieldDefault() {
let app = createTemporaryObject(myFormComponent, root)
verify(!!app, "Component exists")
let emailField = findChild(app, "emailField")
verify(!!emailField, "Object exists")
compare(emailField.text, qsTr(""))
}
源码 QML 必须为测试要触达的每个后代声明 objectName: "..."(详见规则 46)。
4. 属性测试(规则 6–11)
规则 6:background 属性使用 .background 访问器(例如 dial.background、dial.background.color),不要直接把 background 当字符串或颜色值断言。同样的模式适用于 contentItem、indicator 等以 Item 子对象形式暴露的风格钩子。
规则 7:只为源码中显式定义的属性提出测试。 不要为组件继承但未覆盖的属性发明测试(见 qt-quick-test-properties.md)。测试"契约默认值"(下游代码依赖、翻转默认值会改变可观察行为的值),跳过实现细节默认值(字号、内边距、派生尺寸、样式链值)。
规则 8–11:明确禁止测试的 4 类属性:
| 规则 | 禁止项 | 理由 |
|---|---|---|
| 8 | appControl 尺寸 |
顶层尺寸由测试宿主窗口决定,与源码无关,断言会产生 flaky 测试 |
| 9 | anchors 属性 |
anchors 描述的是布局意图,不是要验证的值 |
| 10 | currentIndex |
与模型状态强耦合,应测试其可观察的副作用(如选中变化时发出的信号) |
| 11 | cursorVisible |
由焦点与闪烁定时器驱动,单元测试无法可靠观察 |
5. 信号与 SignalSpy(规则 12–22)
规则 12:只为被测源码中定义的信号添加 SignalSpy。 每个信号在独立的测试函数中测试。先设置 spy 目标再执行触发动作:clickSpy.target = button;更换目标前必须先清除:clickSpy.clear()。spy 在动作之后才挂接会导致 count: 0 并触发 tryCompare 超时。
规则 13(Slider 信号)、规则 14(SpinBox 信号):均遵循规则 12。 注意 Slider.moved、SpinBox.valueModified 是"仅交互"信号——只有触摸、鼠标、滚轮或按键等真实用户输入才会触发,程序化赋值只触发 valueChanged(详见 qt-quick-test-controls.md 的"Interactive-only signals"表)。
规则 15:对 valueModified 信号的 SignalSpy 不要用 wait 方法,改用 tryCompare(spy, "count", N)。
规则 16:MenuItem 信号——点击之前先打开菜单,然后比较 spy 值,否则仍按规则 12 处理:
menu.open()
mouseClick(openItem)
tryCompare(triggeredSpy, "count", 1)
规则 17:TapHandler / HoverHandler 信号——同样遵循规则 12,且必须通过 mouseClick(<hostItem>) 触发,不能直接调用信号函数(见规则 43)。
规则 18:Accessible 类型信号(如 onPressAction)——同规则 12。 正常驱动底层控件,无障碍信号作为副作用触发,spy 目标为 button.Accessible。
规则 19:Dialog、FileDialog、FolderDialog、ColorDialog、MessageDialog 信号——同规则 12。 对话框本身就是被测组件,直接创建它(不经过 findChild 步骤),spy accepted/rejected 等信号,用 dialog.standardButton(Dialog.Ok) 找到标准按钮后点击驱动(规则 39)。
规则 20:MouseArea 信号——同规则 12。 可测试 clicked、pressed、released、positionChanged、entered、exited,拖拽场景用"按下/移动/释放"序列(规则 24)。
规则 21–22:每个目标使用独立的 SignalSpy 实例,并带描述性 ID(如 ageAcceptedSpy、priceAcceptedSpy)。测试多个相似控件时同样如此,各自独立设置 target。若复用同一 spy 跨测试函数,必须 clear();在新测试函数中用全新 spy 时 clear() 依然安全且推荐显式调用(qt-quick-test-template.md 的 Variant 4 给出了双 SpinBox 的完整示例)。
6. 鼠标与键盘事件(规则 23–29)
规则 23:测试输入组件前先设置焦点:emailField.focus = true。 TextField、TextArea、TextInput、TextEdit 需要焦点才能接受输入。
规则 24:Button 取消信号与 MouseArea 的 onPositionChanged,用如下序列模拟:
mousePress(button, button.width / 2, button.height / 2)
mouseMove(button, -10, -10)
mouseRelease(button, -10, -10)
-10, -10 故意落在按钮边界之外,使按下被解释为"取消"而非"点击"。上面的序列只是动作,不是测试本身——之后必须跟一个针对结果的断言:要么 tryCompare(canceledSpy, "count", 1)(spy 指向源码的 canceled 信号),要么 tryCompare 被按下操作改动的属性,确认释放处理器的恢复路径没有运行(源码在释放点落在命中区外时发出的是 canceled 而非 released,因此 onPressed 设置的状态预期保持)。没有后续断言的 press/move/release 是空操作——见规则 47。
规则 25:禁止用 keyClick() 测试文本输入。 应先把焦点设为 true,再把期望字符串直接赋给 text 属性。keyClick 产生的是单键事件,与正常文本输入驱动方式不符,验证不可靠。
规则 26:用 mouseDoubleClickSequence 而不是 mouseDoubleClick。
规则 27:任何鼠标事件之后、对属性或 spy 的断言一律使用 tryCompare()——不仅限于 release 或 doubleclick,mousePress、mouseMove、mouseRelease、mouseClick、mouseDoubleClickSequence 之后都适用:
mousePress(button, button.width / 2, button.height / 2)
tryCompare(shadowEffect, "shadowVerticalOffset", 2)
tryCompare(pressedSpy, "count", 1)
为何要扩大适用范围:同步 JS 处理器(onPressed: shadowEffect.shadowVerticalOffset = 2)用即时 compare 没问题,但一旦源码加上 Behavior on shadowVerticalOffset 或 NumberAnimation,绑定就变成异步的,即时 compare 会与之竞争。tryCompare 轮询最长 5 秒,可以经受住将来的这类改动。wait(100) 是更弱的启发式——在能用 tryCompare 替代 wait + compare 的每个位置都优先用它。普通 compare 只保留给任何输入事件之前的状态读取。
规则 28:对由焦点变化触发的属性更新,在断言结果之前显式设置 focus = false 或 focus = true。
规则 29:不要使用 Qt.Key_At、Qt.Key_Dollar、Qt.Key_Percent、Qt.Key_Hash。 这些键映射到平台与键盘布局相关的扫描码,结果不稳定。需要特殊字符时直接通过 text 属性赋值。
7. 代码约定(规则 30–33)
规则 30:compare 和 verify 中不允许自定义消息,除了以下三种规范形式(所有模板变体都在用):
verify(!!x, "Object exists")——findChild之后,以及createTemporaryObject返回的非顶层组件句柄。verify(!!x, "Component exists")——单组件测试中顶层组件的实例化。verify(comp.status === Component.Ready, comp.errorString())——Variant 7 中Qt.createComponent的状态检查;errorString()是消息的诊断价值所在,必须保留。
其余所有 verify 和 compare 调用都不带消息。Variant 7 中 createObject 之后的检查是 verify(win !== null),不带字符串——不要扩写为 verify(win !== null, "Window exists")。同一测试函数内混用裸形式与字符串形式是需要避免的不一致。
规则 31:颜色使用小写十六进制:'#ff0000';用 '#00000000' 而不是 'transparent'。
规则 32:测试函数中使用标准 JavaScript 十进制记法:99.99,不要用语言环境相关的小数逗号 99,99。
规则 33:文本值使用 qsTr() 包裹:qsTr("Button1")。
8. 控件专项(规则 34–39)
规则 34:TextArea、TextEdit、TextInput、TextField:测试多种输入,包括字符、数字和特殊字符。每类输入一个独立测试函数(qt-quick-test-controls.md 中的示例覆盖了 "Alice"、"12345"、"Name & Co. (1985)" 三种类别)。
规则 35:Dial:添加一个通过模拟手柄移动来验证值变化的测试用例。 优先用鼠标拖拽而非 keyClick 驱动:单次拖拽会触发 moved 多次(mouseMove 期间一次、mouseRelease 再一次),所以断言 spy 计数用 >= 1,而不是 tryCompare(spy, "count", 1);拖拽落点取决于 Dial 几何与 inputMode,比较 value !== before 而非具体数值。
规则 36:NumberAnimation:用 tryCompare 确保动画完成后再验证值。 动画是异步的,不要在启动动画后立即断言属性;若动画驱动链式属性,用大于动画时长的 tryCompare 超时断言最终值。
规则 37:Image:包含图片是否成功加载的测试。 Image.Ready 状态表示加载成功:tryCompare(logo, "status", Image.Ready);若源码声明了显式 source 属性,再验证它解析到预期 URL。
规则 38:RegularExpressionValidator:同时测试正确与不正确的输入值。 输入的 acceptableInput 反映当前文本是否通过校验:
emailField.focus = true
emailField.text = qsTr("user@example.com")
verify(emailField.acceptableInput)
emailField.focus = true
emailField.text = qsTr("not-an-email")
verify(!emailField.acceptableInput)
规则 39:Dialog 标准按钮用 let okButton = dialog.standardButton(Dialog.Ok) 查找。 Dialog.Ok 是 QtQuick.Controls 的枚举值,因此引用它的测试需要 import QtQuick.Controls(规则 1)。
9. 属性依赖(规则 40)
规则 40:当属性的值依赖不在测试范围内的 QML 组件时,排除对该属性的测试。 例如属性绑定到由父组件提供、测试作用域未加载的组件或外部模型(如 formattedPrice: priceFormatter.format(value) 而 priceFormatter 不在测试作用域内),就跳过,改为直接测试可验证的属性。拿不准时,宁可省略也不写脆弱测试。
本规则同样适用于属性的有效值由 State { PropertyChanges { ... } } 块设置的情况:设置兄弟绑定的右端并不会翻转绑定的输出——激活状态的 PropertyChanges 会覆盖它。测试应驱动状态的条件属性(guard property),或跳过该断言。典型例子(qt-quick-test-pitfalls.md):源码声明 states: [ State { when: ...; PropertyChanges { swipe.enabled: true } } ] 时,绑定输出由哪个状态激活决定,而非兄弟绑定 swipe.enabled: !root.done 决定——测试要驱动状态条件(如 isEditMode)。
10. Window 与 Singleton 源码(规则 41–42)
规则 41:Window / ApplicationWindow 源码不得通过 createTemporaryObject(component, root) 实例化——窗口会忽略 Item 父对象、变成顶层窗口,破坏 when: windowShown 语义。改用 Qt.createComponent(<url>) + comp.createObject(null, { requiredProperty: … }),必须提供源码声明的每个 required property(缺失时加载会静默失败,win 为 null);每个测试函数结束时销毁 win 和 comp(测试框架不会替你 GC 窗口)。
URL 形式的选取是规则 41 的关键一环,其唯一权威依据是 qt-quick-test-template.md 中"Variant 7 — Window / ApplicationWindow"下的按源码位置选择 URL 的表:
| 源码 QML 模块的后端 | 使用的 URL |
|---|---|
qt_add_library(... STATIC|SHARED) + qt_add_qml_module(<lib> URI "<URI>" ...) |
"qrc:/qt/qml/<URI>/<File>.qml" |
qt_add_executable(<exe>) + qt_add_qml_module(<exe> URI "<URI>" ...) |
Qt.resolvedUrl("../<File>.qml") |
| 磁盘上的源码,无 CMake 信息 | Qt.resolvedUrl("../<File>.qml") |
三种形式的取舍:qrc 形式能扛住符号链接、-input <other-dir> 运行和测试树搬迁,但当源码属于可执行文件后端的模块时,从兄弟测试二进制中不可达(qrc 在应用里,不在测试里);Qt.resolvedUrl("../<File>.qml") 可移植且与机器无关,只在符号链接测试树下会失效(URL 会相对符号链接而非其目标解析);绝对 file:/// 仅作为该符号链接场景的逃生口,永远不要作为默认——它硬编码了机器布局并嵌入开发者主目录。
Variant 7 还有一个重要提示:优先用鼠标事件而非 keyClick 做输入断言。派生的 win 是运行器自身 TestCase 窗口之外的第二个顶层窗口,在 Wayland、macOS 和 offscreen 平台上不会自动成为活动窗口——合成按键会进入运行器的窗口而不是 win;forceActiveFocus() 只能在活动窗口内强制焦点,救不了这种情况。mouseClick、mousePress/mouseMove/mouseRelease 直接投递到目标 item,绕过了窗口激活路径。
规则 42:标记了 pragma Singleton 的源码(或 CMake 中以 QT_QML_SINGLETON_TYPE TRUE 注册的)按名称访问,不实例化。导入该单例所属模块后直接读写其属性(MySingleton.someProperty = …)。绝不要把单例包进 Component { MySingleton {} }——QML 引擎会拒绝实例化单例类型,测试文件编译失败。 由于单例是进程级的、跨测试函数持续存在,每个测试函数结束时必须恢复被改写的可写属性,使其他测试看到已知状态:
function test_disabledWritable() {
let original = MyConfig.disabled
MyConfig.disabled = true
compare(MyConfig.disabled, true)
MyConfig.disabled = false
compare(MyConfig.disabled, false)
MyConfig.disabled = original
}
11. 触发指针处理器信号(规则 43)
规则 43:绝不要把指针处理器的信号当函数调用。 TapHandler.tapped、HoverHandler.hoveredChanged、DragHandler.translationChanged 等携带必需的 (QEventPoint, Qt::MouseButton) 参数;把它们当函数调用会抛出 Insufficient arguments 并使测试失败。应通过对持有该处理器的可视 item 执行 mouseClick(<hostItem>, …) 来驱动处理器。对于不渲染的宿主(如 Image { source: "" }),先给它一个可测量的尺寸(见规则 44)。
典型失败症状(qt-quick-test-pitfalls.md):tryCompare(tappedSpy, "count", 1) 以 Uncaught exception: Insufficient arguments 失败,通常发生在几毫秒内——QML 无法为裸调用合成事件点/按钮载荷。
12. 点击目标的尺寸(规则 44)
规则 44:对使用隐式尺寸/布局驱动尺寸的受测类型,在行内 Component 定义上设置显式 width 和 height。 在 QT_QPA_PLATFORM=offscreen 下,由 Layout.fillWidth、anchors 或隐式度量决定的实例可能在合成点击派发时是 0×0;此时 tryCompare 会耗尽 5 秒超时。implicitWidth/implicitHeight 只是起点,不是尺寸保证。正确写法:
Component {
id: myButtonComponent
MyButton { width: 100; height: 40; text: qsTr("Press") }
}
识别信号:正常点击测试通常约 100 ms 完成,而 5 秒 tryCompare 超时就是 0×0 实例的典型症状。
仓库佐证:本仓库的 Qt Quick Test 集成恰好印证了 offscreen 主题——test/UnitTestFramework/QmlTesting/CMakeLists.txt 中
add_test(NAME QmlQuickTests COMMAND $<TARGET_FILE:QGCQmlQuickTests> -platform offscreen ...),并设置环境QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software。在该平台下,依赖布局派生的尺寸进行合成点击是不可靠的,规则 44 的显式尺寸策略正是为此设计。
13. Qt Quick 3D 源码处理(规则 45)
规则 45:Qt Quick 3D 图形节点源码(Model、Node、*Camera、*Light、Skybox、SceneEnvironment 等需要场景的 QtQuick3D 节点类型)如果被包进标准的 Item { id: root } 模板,对象会在任何图形场景之外创建——测试报告 PASS,但运行时会触发 QWARN: Created graphical object was not placed in the graphics scene。直接跳过这些源码,并在最终回复中注明类型。以 View3D 为根的源码和 *Material 类型则落入标准模板。
14. 不可达的内部 item(规则 46)
规则 46:测试会实质驱动的内部 item 必须声明 objectName;只有 id 的 item 在兄弟测试文件中不可达。 QML 的 id 只在声明它的文档内有效——findChild 及一切跨文档查找都按 objectName 解析。当源码中值得测试的子项(会发信号的控件、运行时被源码改写的属性、交互时值会变的效果)缺少 objectName 时:
- 向用户询问一次是否添加
objectName声明并扩展覆盖范围。 - 若接受:应用最小源码编辑——每个 item 一行
objectName: "<id>",与现有id一致,在同一 item 上,不做其他改动——然后基于编辑后的源码生成。 - 若 item 已带不匹配的
objectName:不要覆盖,用现有名字触达它。 - 若用户拒绝或无用户可用:跳过受影响的断言,并在最终回复中按
id+ 源码行号列出每个未触达的 item 及所需的一行编辑。
外层作用域属性无需 objectName 仍可测试,应继续覆盖:Window 的 title/visible、根组件 property 别名、单例属性(规则 42)。不要退回到脆弱的工作区——parent.children 索引、toString() 匹配或可视树遍历会因错误的原因通过测试,并在一次外观级源码改动后失效。
本规则适用于嵌套组件变体(Variant 2)和 Window/ApplicationWindow 变体(Variant 7);单组件变体(Variant 1)与单例变体(Variant 8)不触达子项,不受影响。
仓库佐证:QGC 的示例测试 test/UnitTestFramework/QmlTesting/tests/tst_Example.qml 直接在被测
Rectangle上声明id并同文档内引用——这是同一文档内id生效的合法场景;而跨文档的findChild则必须依赖objectName。其结构(TestCase { id: testCase; name: "ExampleQmlTest" }、function test_xxx()、SignalSpy、cleanup())也与 QmlTesting.h 记载的"一个文件一个组件、用 SignalSpy 处理异步、测试相互独立"最佳实践一致。
15. 无断言测试函数(规则 47)
规则 47:每个测试函数必须至少包含一个针对"动作预期改变的状态"的断言。 compare、tryCompare 或 tryCompare(spy, "count", N) 都算;存在性检查(verify(!!x, "Object exists"))只算前置条件,不算测试体。一个只有 setup + 动作 + wait 的函数无论行为如何都会通过——它什么都没测。
典型症状:test_* 函数改了输入(鼠标、键盘、属性写入)却以不含 compare/tryCompare(除存在性检查外)结束。运行器报告 PASS,而动作处理器里真正的回归也会报告 PASS——测试形同虚设。
解决方案——命名期望的"动作后状态"并断言它:
- 指针处理器/信号流:瞄准
SignalSpy并tryCompare其count。 - 属性变更:
tryCompare该属性到期望的新值。 - 不应改变属性的路径(如取消序列中
onReleased不得运行):tryCompare该属性到它应保持的未变值。
生成测试前的机械检查:扫描每个 test_* 函数体,若含鼠标、键盘或属性写入语句,却没有除存在性检查外的 compare/tryCompare,要么补上缺失的断言,要么删除该测试——不要原样发出。
16. 规则体系在 QGroundControl 中的落地形态
以上 47 条规则最终服务于"生成可运行、可验证的 Qt Quick Test 测试文件"这一目标。在本仓库中,生成与运行是分工的:qt-qml-test 技能负责生成(输出契约见 SKILL.md:默认写到 tests/tst_<ComponentName>.qml,若文件已存在须询问用户,完成后只报告创建路径、不倾倒代码);构建与运行则由仓库的 Qt Quick Test 基础设施承担:
- 测试文件命名必须为
tst_*.qml,放在 test/UnitTestFramework/QmlTesting/tests/; - QmlQuickTestMain.cc 仅一行
QUICK_TEST_MAIN(QGCQmlQuickTests),生成独立进程的测试可执行程序; - CMakeLists.txt 通过
add_test(NAME QmlQuickTests ...)接入 CTest,运行参数与规则 44 的 offscreen 场景完全对应(-platform offscreen+QT_QPA_PLATFORM=offscreen),并用FAIL_REGULAR_EXPRESSION "FAIL!;QFATAL;Segmentation fault;ASSERT"捕获失败信号;测试通过ctest -R QmlQuickTests运行; - QmlTestFileValidator.cc 提供进程内结构校验:扫描
tst_*.qml,确认每个文件包含TestCase与function test_——这与规则 30、规则 47 强调的"测试体必须有真断言"互为补充; - QmlTesting.h 整理了可用测试 API 清单(
compare、verify、tryCompare、tryVerify、mouseClick、keyClick、wait、waitForRendering)与文件命名规范。
由此,规则的完整闭环是:按 47 条规则生成 tst_*.qml → 落入 tests/ 目录 → QGCQmlQuickTests 在 offscreen 平台运行 → CTest 汇总结果。掌握规则即可写出与仓库测试体系无缝衔接的 QML 单元测试。
17. 小结:从规则到可靠测试
47 条规则可以浓缩为几条贯穿性原则:
- 一切输入事件都要落到可视树上——父对象必须是显式尺寸的
rootItem,点击目标必须有非零显式尺寸(规则 3、44); - 异步是常态——鼠标事件后的断言、动画完成、异步组件加载统一交给轮询 5 秒的
tryCompare(规则 27、36、41); - 只测契约——只测源码显式定义的东西,不测宿主决定的尺寸、布局意图、时序敏感状态(规则 7–11、40);
- 信号驱动靠真实交互——
SignalSpy只挂源码声明的信号、每个信号独立函数、用真实输入事件而非直接调用信号函数驱动(规则 12–22、43); - 测试必须有断言——没有
compare/tryCompare的test_*函数等于没写(规则 47)。
在实际的 QGroundControl 仓库中,这套规则体系直接支撑着 test/UnitTestFramework/QmlTesting/ 下的 QML 组件测试工作流。动手实践时,可以先阅读 qt-quick-test-template.md 选择与源码形态匹配的模板变体,再逐条核对本规则文档,最后用仓库的 CTest 集成验证结果。