Qt Test 最佳实践
我们建议您为错误修复和新功能添加 Qt Test。在尝试修复错误之前,添加一个回归测试(最好是自动测试),该测试在修复错误之前会失败,而在修复之后会通过。在开发新功能时,添加测试以验证它们是否按预期运行。
遵守一套编码标准将使 Qt 自动测试更有可能在所有环境中可靠地工作。例如,有些测试需要从磁盘读取数据。如果不对读取数据的方式制定标准,有些测试就无法移植。例如,假设测试数据文件在当前工作目录下的测试只适用于源代码内的构建。在影子编译(源代码目录外)中,测试将无法找到数据。
以下各节包含编写 Qt 测试的指导原则:
一般原则
以下章节提供了编写单元测试的一般原则:
验证测试
编写测试并将其与修复或新功能一起提交到新分支。完成后,您可以签出您的工作所基于的分支,然后将新测试的测试文件签出到该分支。这样就能验证测试是否在前一个分支上失败,从而真正捕捉到错误或测试新功能。
例如,如果使用 Git 版本控制系统,修复QDateTime
类错误的工作流程可能是这样的:
- 为修复和测试创建一个分支:
git checkout -b fix-branch 5.14
- 编写测试并修复错误。
- 同时使用修复和新测试进行构建和测试,以验证新测试是否与修复一起通过。
- 将修复和测试添加到分支:
git add tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp src/corelib/time/qdatetime.cpp
- 将修复和测试提交到分支:
git commit -m 'Fix bug in QDateTime'
- 为了验证测试是否真的捕捉到了你需要修复的内容,请签出你的分支:
git checkout 5.14
- 仅将测试文件签出到 5.14 分支:
git checkout fix-branch -- tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp
现在只有测试在修复分支上。源代码树的其他部分仍在 5.14 分支上。
- 编译并运行测试,验证它是否在 5.14 版本上失败,从而确实捕捉到了一个错误。
- 现在就可以返回修复分支了:
git checkout fix-branch
- 或者,你也可以将工作树恢复到 5.14 版上的干净状态:
git checkout HEAD -- tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp
当你审核一项变更时,你可以调整这个工作流程,以检查该变更是否确实附带了针对其修复的问题的测试。
给测试函数起描述性的名称
给测试用例命名很重要。测试名称会出现在测试运行的失败报告中。对于数据驱动测试,数据行的名称也会出现在失败报告中。这些名称能让阅读报告的人初步了解出错的原因。
测试功能名称应明确说明该功能要测试什么。不要简单地使用错误跟踪标识符,因为如果更换错误跟踪器,标识符就会过时。此外,有些错误跟踪器并非所有用户都能访问。如果错误报告可能会引起测试代码后来读者的兴趣,可以在测试相关部分的注释中提及。
同样,在编写数据驱动测试时,给测试用例起一个描述性的名称,说明每个测试用例关注功能的哪个方面。不要简单地给测试用例编号,或使用错误跟踪标识符。阅读测试输出结果的人根本不知道这些数字或标识符是什么意思。如果相关,您可以在测试行上添加注释,提及错误跟踪标识符。最好避免使用间距字符和可能对命令行 shell 有影响的字符,因为你可能要在命令行 shell 上运行测试。这样可以更方便地在测试程序的命令行中指定测试和标记,例如,将测试运行限制为一个测试用例。
编写独立的测试函数
在测试程序中,测试功能应相互独立,不应依赖于先前已运行过的测试功能。您可以使用tst_foo testname
单独运行测试函数来检查这一点。
不要在多个测试中重复使用被测类的实例。测试实例(如部件)不应成为测试的成员变量,最好在堆栈中实例化,以确保即使测试失败也能正确清理,从而避免测试相互干扰。
如果测试涉及全局更改,无论测试通过还是失败,都要注意确保在测试结束时恢复之前的状态。由于测试失败会阻止晚于失败检查的代码运行,因此当测试失败时,在测试结束时还原是不起作用的。即使在失败时也能还原的稳健方法是实例化一个 RAII 对象,其析构函数会还原之前的状态。这通常可以通过qScopeGuard 方便地实现,例如
const auto restoreDefaultLocale = qScopeGuard([prior = QLocale()]() { QLocale::setDefault(prior); });
在测试中首次调用QLocale::setDefault() 之前恢复先前状态,该测试需要控制被测代码使用的本地语言。
测试全栈
如果应用程序接口是通过可插拔或特定平台的后端来实现的,那么在编写测试时,一定要将代码路径一直覆盖到后端。使用模拟后端测试上层应用程序接口部分是将应用程序接口层中的错误与后端隔离开来的一种好方法,但它与使用真实数据运行实际实现的测试是互补的。
让测试快速完成
测试不应因不必要的重复、使用不适当的大量测试数据或引入不必要的闲置时间而浪费时间。
单元测试尤其如此,单元测试执行时间每增加一秒,就会使跨多个目标分支的 CI 测试花费更多时间。请记住,单元测试与负载和可靠性测试是不同的,后者需要更多的测试数据和更长的测试运行时间。
基准测试通常会多次执行同一测试,应位于单独的tests/benchmarks
目录中,不应与功能单元测试混用。
使用数据驱动测试
数据驱动测试可以更方便地针对后来的错误报告中发现的边界条件添加新的测试。
使用数据驱动测试而不是在测试中依次测试多个项目,可以避免重复编写非常相似的代码,并确保即使前面的测试失败,后面的案例也能得到测试。它还鼓励系统化和统一的测试,因为对每个数据样本都采用了相同的测试。
如果测试是数据驱动的,您可以在测试的命令行中指定其 data-tag 和测试功能名称(如function:tag
),以便只在一个特定的测试用例上运行测试,而不是在该功能的所有测试用例上运行测试。这既可用于全局数据标签,也可用于局部标签,从函数自身的数据中识别一行;甚至还可以将它们组合起来,如function:global:local
。
使用覆盖工具
使用覆盖工具(如Coco或gcov)帮助编写测试,尽可能多地覆盖被测函数或类中的语句、分支和条件。在新功能的开发周期中越早这样做,以后重构代码时就越容易捕捉到回归。
选择适当的机制来排除测试
选择适当的机制来排除不适用的测试非常重要。
使用QSKIP() 处理运行时发现整个测试函数在当前测试环境中不适用的情况。当只跳过测试功能的一部分时,可使用条件语句,也可选择调用qDebug()
报告跳过不适用部分的原因。
当已知的测试失败最终应被修复时,建议使用QEXPECT_FAIL ,因为它支持在可能的情况下运行测试的其余部分。它还能验证问题是否仍然存在,并在代码维护者无意中修复问题时让他们知道,即使使用Abort 标志也能获得这种好处。
数据驱动测试的测试函数或数据行可以限制在特定平台上,或限制在使用#if
启用的特定功能上。不过,在使用#if
跳过测试功能时要注意moc的限制。moc
预处理器无法访问编译器的所有builtin
宏,而这些宏通常用于编译器的功能检测。因此,对于预处理器条件,moc
可能会得到与代码其他部分不同的结果。这可能导致moc
生成实际编译器跳过的测试槽的元数据,或省略实际编译到类中的测试槽的元数据。在第一种情况下,测试将尝试运行一个未实现的测试槽。在第二种情况下,即使应该运行测试槽,测试也不会尝试运行。
如果整个测试程序不适用于特定平台,或除非启用了特定功能,最好的办法是使用父目录的构建配置来避免构建测试。例如,如果tests/auto/gui/someclass
测试程序不适用于 macOS,可将其作为tests/auto/gui/CMakeLists.txt
的子目录,并进行平台检查:
if(NOT APPLE) add_subdirectory(someclass) endif
或者,如果使用qmake
,则在tests/auto/gui.pro
中添加以下一行:
mac*: SUBDIRS -= someclass
另请参阅使用 QSKIP 跳过测试。
避免 Q_ASSERT
Q_ASSERT 宏会在断言条件为false
时导致程序中止,但前提是软件是在调试模式下构建的。在发布版和调试发布版中,Q_ASSERT
都不起作用。
Q_ASSERT
应避免使用这种方法,因为它会根据是否测试调试构建而使测试行为不同,而且会导致测试立即中止,跳过所有剩余的测试功能,并返回不完整或畸形的测试结果。
它还会跳过本应在测试结束时进行的任何拆除或整理工作,因此可能会使工作区处于不整洁状态,这可能会给后续测试带来麻烦。
应使用QCOMPARE() 或QVERIFY() 宏变量来代替Q_ASSERT
。它们会导致当前测试报告失败并终止,但允许执行其余的测试功能,并使整个测试程序正常终止。QVERIFY2() 甚至允许在测试日志中记录描述性错误信息。
编写可靠的测试
以下章节提供了编写可靠测试的指南:
避免验证步骤中的副作用
在自动测试中使用QCOMPARE(),QVERIFY() 等执行验证步骤时,应避免产生副作用。验证步骤中的副作用会使测试难以理解。此外,当测试改为使用QTRY_VERIFY(),QTRY_COMPARE() 或QBENCHMARK() 时,它们很容易以难以诊断的方式破坏测试。它们可以多次执行传递的表达式,从而重复产生任何副作用。
当副作用不可避免时,即使测试失败,也要确保在测试函数结束时恢复之前的状态。这通常需要使用 RAII(资源获取即初始化)类,在函数返回时恢复状态,或使用cleanup()
方法。不要简单地将恢复代码放在测试的末尾。如果部分测试失败,这些代码将被跳过,之前的状态也不会恢复。
避免固定超时
避免使用硬编码超时,如QTest::qWait() 来等待某些条件成真。可考虑使用QSignalSpy 类、QTRY_VERIFY() 或QTRY_COMPARE() 宏,或将QSignalSpy
类与QTRY_
宏变体结合使用。
qWait()
函数可用于在执行某些操作和等待该操作触发的某些异步行为完成之间设置一个固定的延迟时间。例如,改变部件的状态,然后等待重新绘制部件。然而,当在工作站上编写的测试在设备上执行时,这种超时往往会导致失败,因为设备上的预期行为可能需要更长时间才能完成。在最慢的测试平台上,将固定超时值增加到比所需值大几倍并不是一个好的解决方案,因为这会减慢在所有平台上的测试运行速度,尤其是对于表格驱动的测试。
如果被测代码在完成异步行为时发出 Qt 信号,更好的办法是使用QSignalSpy 类通知测试函数现在可以执行验证步骤。
如果没有 Qt 信号,则使用QTRY_COMPARE()
和QTRY_VERIFY()
宏,它们会周期性地测试指定条件,直到该条件变为真或达到某个最大超时。这些宏可以防止测试时间超过所需的时间,同时避免在工作站上编写测试后在嵌入式平台上执行时出现中断。
如果没有 Qt 信号,而你正在编写测试作为开发新 API 的一部分,请考虑 API 是否能从添加一个报告异步行为完成的信号中受益。
谨防定时行为
某些测试策略很容易受到某些类的定时相关行为的影响,这可能导致测试仅在某些平台上失败,或无法返回一致的结果。
其中一个例子是文本输入部件,它通常有一个闪烁的光标,可以根据捕获位图时光标的状态,使捕获位图的比较成功或失败。这反过来又取决于执行测试的机器速度。
在测试根据定时器事件改变状态的类时,需要在执行验证步骤时考虑基于定时器的行为。由于与定时相关的行为多种多样,因此并不存在解决这一测试问题的单一通用方案。
对于文本输入部件,潜在的解决方案包括禁用光标闪烁行为(如果应用程序接口提供了该功能)、在捕获位图之前等待光标处于已知状态(例如,如果应用程序接口提供了适当的信号,则通过订阅该信号),或将包含光标的区域排除在位图比较之外。
避免位图捕获和比较
虽然通过捕获和比较位图来验证测试结果有时是必要的,但它可能相当脆弱且耗费人力。
例如,特定的 widget 在不同的平台上或使用不同的 widget 样式时可能会有不同的外观,因此可能需要多次创建参考位图,然后在将来随着 Qt 支持的平台集的发展而进行维护。因此,对位图进行修改意味着必须在每个支持的平台上重新创建预期位图,这就需要访问每个平台。
位图比较也会受到一些因素的影响,如测试机的屏幕分辨率、位深度、活动主题、配色方案、部件样式、活动地域(货币符号、文本方向等)、字体大小、透明度效果以及窗口管理器的选择。
在可能的情况下,使用编程手段,如验证对象和变量的属性,而不是捕捉和比较位图。
改进测试输出
以下章节提供了制作可读和有用的测试输出的指导原则:
测试警告
就像在构建软件时一样,如果测试输出中充斥着大量警告,您就很难注意到真正是错误线索的警告。因此,谨慎的做法是定期检查测试日志中的警告和其他无关输出,并调查其原因。当它们是错误的征兆时,可以让警告触发测试失败。
当被测代码应该产生信息(如关于错误使用的警告)时,同样重要的是测试它在使用时是否会产生这些信息。您可以使用QTest::ignoreMessage() 测试由qWarning()、qDebug()、qInfo() 和其他程序产生的预期消息。这将验证是否产生了信息,并将其从测试运行的输出中过滤掉。如果未生成消息,测试将失败。
如果只有在调试模式下构建 Qt 时才会输出预期消息,请使用QLibraryInfo::isDebugBuild() 来确定 Qt 库是否在调试模式下构建。使用#ifdef QT_DEBUG
是不够的,因为它只能告诉您测试是否是在调试模式下构建的,而这并不能保证Qt 库也是在调试模式下构建的。
您的测试可以(自 Qt 6.3 起)通过调用QTest::failOnWarning() 来验证它们没有触发对qWarning() 的调用。该调用接收要测试的警告信息或与警告匹配的QRegularExpression ;如果产生了匹配的警告,就会报告并导致测试失败。例如,本应不产生任何警告的测试可以使用QTest::failOnWarning(QRegularExpression(u".*"_s))
,它将匹配任何警告。
您也可以设置环境变量QT_FATAL_WARNINGS
,将警告作为致命错误处理。详情请参阅qWarning() ;这并不是自动测试所特有的。如果警告会在浩瀚的测试日志中消失,那么偶尔使用此环境变量可以帮助您找到并消除出现的警告。
避免从自动测试中打印调试信息
自动测试不应产生任何未处理的警告或调试信息。这将允许 CI Gate 将新的警告或调试信息视为测试失败。
在开发过程中添加调试信息是可以的,但应在测试签入前禁用或删除这些信息。
编写结构良好的诊断代码
任何在测试失败时有用的诊断输出都应成为常规测试输出的一部分,而不是被注释掉、被预处理器指令禁用或仅在调试构建时启用。如果测试在持续集成过程中失败,那么在 CI 日志中记录所有相关的诊断输出,就能比启用诊断代码并重新测试节省很多时间。尤其是如果测试失败发生在您桌面上没有的平台上。
测试中的诊断信息应使用 Qt 的输出机制,如qDebug()
和qWarning()
,而不是stdio.h
或iostream.h
输出机制。后者会绕过 Qt 的消息处理机制,并阻止-silent
命令行选项抑制诊断消息。这可能导致重要的故障信息被隐藏在大量的调试输出中。
编写可测试代码
以下章节提供了编写易于测试的代码的指导原则:
打破依赖关系
单元测试的理念是孤立地使用每个类。由于许多类会实例化其他类,因此不可能单独实例化一个类。因此,您应该使用一种称为依赖注入的技术,将对象创建与对象使用分离开来。工厂负责构建对象树。其他对象通过抽象接口来操作这些对象。
这种技术对于数据驱动型应用程序非常有效。对于图形用户界面应用程序来说,这种方法可能比较困难,因为对象的创建和销毁非常频繁。要验证依赖于抽象接口的类的行为是否正确,可以使用mocking。例如,请参见Googletest Mocking (gMock) Framework。
将所有类编译到库中
在中小型项目中,编译脚本通常会列出所有源文件,然后一次性编译出可执行文件。这意味着测试的构建脚本必须再次列出所需的源文件。
在构建静态库的脚本中只列出一次源文件和头文件会更容易。然后,main()
函数将与静态库链接以构建可执行文件,测试将与静态库链接。
对于在构建多个程序时使用相同源文件的项目,将共享类构建为动态链接(或共享对象)库可能更为合适,每个程序(包括测试程序)都可以在运行时加载该动态链接库。同样,将编译过的代码放在一个库中,有助于避免重复描述组合哪些组件来制作不同的程序。
设置测试机
以下章节将讨论测试机设置中常见的问题:
所有这些问题通常都可以通过合理使用虚拟化来解决。
屏幕保护程序
屏幕保护程序会干扰图形用户界面类的某些测试,导致不可靠的测试结果。应禁用屏幕保护程序,以确保测试结果的一致性和可靠性。
系统对话框
操作系统或其他运行中的应用程序意外显示的对话框可能会窃取自动测试相关部件的输入焦点,导致无法证实的故障。
典型问题的例子包括 macOS 上的在线更新通知对话框、病毒扫描仪发出的错误警报、预定任务(如病毒签名更新)、推送到工作站的软件更新,以及聊天程序在堆栈顶部弹出窗口。
显示器使用情况
有些测试会使用测试机的显示屏、鼠标和键盘,因此如果测试机同时被用于其他用途或并行运行多个测试,就会导致测试失败。
CI 系统使用专用测试机来避免这个问题,但如果没有专用测试机,也可以通过在第二台显示器上运行测试来解决这个问题。
在 Unix 上,也可以在嵌套或虚拟 X 服务器(如 Xephyr)上运行测试。例如,要在 Xephyr 上运行整套测试,请执行以下命令:
Xephyr :1 -ac -screen 1920x1200 >/dev/null 2>&1 & sleep 5 DISPLAY=:1 icewm >/dev/null 2>&1 & cd tests/auto make DISPLAY=:1 make -k -j1 check
英伟达二进制驱动程序的用户应注意,Xephyr 可能无法提供 GLX 扩展。强制使用 Mesa libGL 可能会有所帮助:
export LD_PRELOAD=/usr/lib/mesa-diverted/x86_64-linux-gnu/libGL.so.1
不过,在 Xephyr 和实际 X 服务器上运行不同 libGL 版本的测试时,QML 磁盘缓存会导致测试崩溃。为避免这种情况,请使用QML_DISABLE_DISK_CACHE=1
。
或者使用 offscreen 插件:
TESTARGS="-platform offscreen" make check -k -j1
窗口管理器
在 Unix 上,至少有两个自动测试(tst_examples
和tst_gestures
)需要运行窗口管理器。因此,如果在嵌套 X 服务器下运行这些测试,还必须在该 X 服务器中运行窗口管理器。
窗口管理器必须配置为在显示屏上自动定位所有窗口。某些窗口管理器(如 Tab 窗口管理器 (twm))具有手动定位新窗口的模式,这样就无法在无用户交互的情况下运行测试套件。
注意: Tab 窗口管理器不适合运行全套 Qt 自动测试,因为tst_gestures
自动测试会导致它忘记配置并恢复到手动窗口放置。
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.