本页内容

Qt Test 最佳实践

我们建议您在修复缺陷和开发新功能时添加 Qt Test。在尝试修复缺陷之前,请先添加一个回归测试(最好是自动的),该测试在修复前应失败(以复现该缺陷),并在修复后通过。在开发新功能时,请添加测试以验证其是否按预期工作。

遵守一套编码标准,将更有助于确保 Qt Test 自动测试在所有环境中都能可靠运行。例如,某些测试需要从磁盘读取数据。如果对此没有设定标准,部分测试将无法实现可移植性。 例如,一个假设测试数据文件位于当前工作目录中的测试,仅在源代码内构建时才有效。在影子构建(源代码目录之外)中,该测试将无法找到其数据。

以下各节包含编写 Qt Test 的指南:

在遵循这些最佳实践的同时,还应牢记关于安全考虑的建议。

一般原则

以下各节提供了编写单元测试的一般指南:

验证测试

在新的分支上编写并提交测试用例,同时提交修复代码或新功能。 完成后,您可以检出您工作所基于的分支,然后将新测试的测试文件检入该分支。这样您就能验证这些测试在原分支上是否确实会失败,从而确认它们确实能捕获 bug 或测试新功能。

例如,如果您使用 Git 版本控制系统,修复QDateTime 类中错误的工作流可能如下所示:

  1. 为修复和测试创建一个分支:git checkout -b fix-branch dev
  2. 编写测试并修复该错误。
  3. 在包含修复和新测试的情况下进行构建和测试,以验证新测试在修复后能够通过。
  4. 将修复代码和测试添加到分支中:git add tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp src/corelib/time/qdatetime.cpp
  5. 将修复代码和测试提交到你的分支:git commit -m 'Fix bug in QDateTime'
  6. 要验证该测试是否确实捕获了您需要修复的问题,请检出您当前分支所基于的原始分支:git checkout dev
  7. 仅从修复分支检出测试文件:git checkout fix-branch -- tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp

    源代码树的其余部分仍保留在 dev 分支上,该分支虽未包含该修复程序,但已准备好使用新测试进行验证。

  8. 构建并运行测试,以验证该测试在 dev 分支上会失败,从而确认它确实捕获了一个 bug。
  9. 现在,你可以返回修复分支:git checkout fix-branch
  10. 或者,你可以将工作树恢复到 `dev` 分支上的干净状态:git checkout HEAD -- tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp

在审查更改时,你可以调整此工作流,以检查该更改是否确实附带了针对其所修复问题的测试。

为测试函数命名时应使用描述性名称

测试用例的命名非常重要。测试名称会显示在测试运行的失败报告中。对于数据驱动测试,数据行的名称也会出现在失败报告中。恰当的名称能让阅读报告的人第一时间了解出了什么问题。

测试函数的名称应能清晰表明该函数试图测试的内容。不要简单地使用缺陷跟踪系统的标识符,因为如果更换了缺陷跟踪系统,这些标识符就会过时。 此外,离线工作的开发人员无法访问您的缺陷跟踪系统,而且某些缺陷跟踪系统可能并非所有用户都能访问。当缺陷报告可能对日后阅读测试代码的人有参考价值时,您可以在测试代码的相关部分旁添加注释提及该报告。

同样地,在编写数据驱动测试时,应为测试用例赋予描述性名称,以表明每个用例侧重于功能的哪个方面。 不要仅仅给测试用例编号,或直接使用缺陷跟踪标识符。阅读测试输出的人将无法理解这些数字或标识符的含义。在相关情况下,你可以在测试行中添加注释,提及缺陷跟踪标识符。 最好避免使用空格字符以及可能对您计划运行测试的命令行 shell 具有特殊含义的字符。这样可以更方便地在命令行中为测试程序指定测试和标签——例如,将测试运行限定为仅执行一个测试用例。

编写自包含的测试函数

在测试程序中,各个测试函数应相互独立,且不应依赖于之前已执行的测试函数。 您可以通过单独运行测试函数(使用 `tst_foo testname`)来验证这一点。对于数据驱动测试,同样应避免测试数据表中各行之间的依赖关系,以便能够使用 `tst_foo function:tag ` 独立运行单行(例如,用于调查失败原因)。

请勿在多个测试中重复使用被测类的实例。测试实例(例如小部件)不应作为测试的成员变量,而应优先在栈上进行实例化,以确保即使测试失败也能正确清理,从而避免测试之间相互干扰。

如果您的测试涉及全局更改,请务必确保在测试结束时恢复先前状态,无论测试通过还是失败。由于失败会阻止失败检查之后代码的执行,因此当测试失败时,在测试结束时进行恢复是行不通的。 即使在测试失败时也能恢复状态的稳健方法是,实例化一个 RAII(资源获取即初始化)对象,其析构函数会恢复先前状态。通常可以使用 `qScopeGuard()` 方便地实现这一点,例如

const auto restoreDefaultLocale = qScopeGuard([prior = QLocale()]() {
    QLocale::setDefault(prior);
});

在需要控制被测代码所用区域设置的测试中,在首次调用QLocale::setDefault() 之前执行此操作。

避免外部依赖

测试函数同样应避免依赖任何外部资源。如果该资源暂时不可用,此类依赖很容易导致测试失败。即使资源可用,它也可能阻塞测试系统的访问——例如,当测试频繁运行时,资源可能会将测试视为对其服务的不受欢迎的负担。

在无法访问时跳过测试可能会掩盖本应通过测试揭示的问题,因此这并非解决此类问题的良策。更好的做法是构建该资源的本地模拟体,使其具备满足测试目的所需的足够特性。

外部依赖性还会给离线工作的开发人员带来问题,也会给在沙箱(例如隔离的虚拟机)中测试来自不可信来源的代码更改带来困难。有关相关问题,请参阅《Qt Test 安全注意事项》。

测试全栈

如果 API 是通过可插拔或平台特定的后端来实现的,且这些后端承担了主要工作,请确保编写测试用例,以覆盖一直深入到后端的全部代码路径。 使用模拟后端测试上层 API 部分,是将 API 层中的错误与后端隔离的一种有效方法,但这仅是对那些使用真实数据(能如实反映实际环境)运行实际实现的测试的补充。

快速完成测试

测试不应因不必要的重复、使用过量的测试数据或引入不必要的空闲时间而浪费时间。

这一点在单元测试中尤为重要,因为单元测试每多执行一秒,就会延长跨多个目标的分支 CI 测试所需的时间。请记住,单元测试与负载和可靠性测试是分开的,后者通常需要更大的测试数据量和更长的测试运行时间。

基准测试通常需要多次执行相同的测试,应将其放置在单独的tests/benchmarks 目录中,且不应与功能单元测试混在一起。

使用数据驱动测试

数据驱动测试使我们能够更轻松地针对后期错误报告中发现的边界条件添加新测试。

使用数据驱动测试而非在测试中依次测试多个项目,可以避免重复编写非常相似的代码,并确保即使早期测试用例失败,后续用例仍能得到测试。此外,由于相同的测试会应用于每个数据样本,这也有助于促进系统化和统一的测试。

当测试采用数据驱动时,您可以在测试的命令行中指定其数据标签以及测试函数名称(例如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。如果这能让您的测试更轻松,那么对 API 的调用者来说也可能会很有用。

警惕与时间相关的行为

某些测试策略容易受到特定类中与时间相关的行为的影响,这可能导致测试仅在某些平台上失败,或者无法返回一致的结果。

文本输入控件便是其中一例:其光标通常会闪烁,这会导致捕获的位图在比较时,根据捕获瞬间光标的状态而出现通过或失败的结果。而这种状态又可能取决于执行测试的机器速度。

在测试那些根据定时器事件改变状态的类时,执行验证步骤时需要将基于定时器的行为考虑在内。由于时间依赖性行为多种多样,因此针对这一测试问题并不存在单一的通用解决方案。

对于文本输入控件,潜在的解决方案包括:禁用光标闪烁行为(如果 API 提供该功能)、在捕获位图前等待光标处于已知状态 (例如,如果 API 提供了相应信号,则通过订阅该信号),或者在位图比较中排除包含光标的区域。

避免捕获和比较位图

虽然有时有必要通过捕获和比较位图来验证测试结果,但这种方法往往非常不稳定且费时费力。

例如,某个小部件在不同的平台上或采用不同的样式时,外观可能会有所不同,因此可能需要多次创建参考位图,并在未来随着 Qt 支持的平台不断演进而进行维护。 因此,进行任何影响位图的更改,就意味着必须在每个受支持的平台上重新创建预期的位图,这需要访问每个平台。

位图比较还可能受到测试机器的屏幕分辨率、位深度、活动主题、配色方案、小部件样式、活动区域设置(货币符号、文本方向等)、字体大小、透明度效果以及窗口管理器的选择等因素的影响。

在可能的情况下,请使用编程手段(例如验证对象和变量的属性),而不是捕获和比较位图。

改进测试输出

以下各节提供了生成易读且有用的测试输出的指导原则:

检查警告

正如构建软件时一样,如果测试输出中充斥着警告,您将更难察觉那些真正预示着错误出现的警告。因此,明智的做法是定期检查测试日志中的警告及其他无关输出,并调查其原因。 当这些是错误的征兆时,你可以让警告触发测试失败。

当被测代码应生成某些消息(例如关于错误使用的警告)时,测试其在被错误使用时确实会生成这些消息也至关重要。 你可以使用QTest::ignoreMessage() 来测试待测代码中由qWarning()、qDebug()、qInfo() 及其相关函数生成的预期消息。这将验证消息是否被生成,并将其从测试运行的输出中过滤掉。如果消息未被生成,测试将失败。

如果预期消息仅在 Qt 以调试模式构建时才会输出,请使用QLibraryInfo::isDebugBuild() 来判断 Qt 库是否以调试模式构建。仅使用#ifdef QT_DEBUG 是不够的,因为它只能告诉你测试本身是否以调试模式构建,但这并不能保证Qt 库也以调试模式构建。

您的测试可以通过调用QTest::failOnWarning() 来验证其是否会触发对qWarning() 的调用。若不带参数(自 Qt 6.8 起),当产生警告时,此调用将导致测试失败并输出该警告。 您还可以选择向 `failOnWarning() ` 传递要检测的警告消息,或指定要匹配的 `QRegularExpression `,从而将此行为限制在匹配的警告上。(这些过滤版本自 Qt 6.3 起引入。)

您还可以设置环境变量QT_FATAL_WARNINGS ,使警告被视为致命错误。详情请参阅qWarning();此功能并非 autotests 所特有。如果警告通常会淹没在庞大的测试日志中,偶尔在设置此环境变量的情况下运行测试,有助于您发现并消除实际出现的警告。

避免在 Autotests 中输出调试信息

通过的自动测试不应产生任何未处理的警告或调试信息。这样,CI Gate 就能将新的警告或调试信息视为测试失败。就像构建代码时编译器发出的警告一样,如果警告很少出现,它们是发现问题的有用线索;但如果它们频繁出现,可能会掩盖开发人员在测试更改时应注意的重要问题。

在开发过程中添加调试信息是可以的,但在将测试代码提交之前,应将其禁用或移除。

编写结构良好的诊断代码

任何在测试失败时有用的诊断输出,都应作为常规测试输出的一部分,而不是被注释掉、通过预处理指令禁用,或仅在调试构建中启用。 如果测试在持续集成过程中失败,将所有相关的诊断输出记录在 CI 日志中,相比于重新启用诊断代码并再次测试,可以为您节省大量时间。特别是当失败发生在您本地计算机上未安装的平台上时。

测试中的诊断消息应使用 Qt 的输出机制,例如qDebug() 和qWarning() ,而非stdio.h 或iostream.h 输出机制。后者会绕过 Qt 的消息处理机制,并导致-silent 命令行选项无法抑制诊断消息。这可能会导致重要的失败消息被海量的调试输出所掩盖。

编写可测试的代码

以下各节提供了编写易于测试的代码的指南:

打破依赖关系

单元测试的核心思想是孤立使用每个类。由于许多类会实例化其他类,因此无法单独实例化某个类。因此,应采用一种称为“依赖注入”的技术,将对象的创建与使用分离。工厂负责构建对象树,其他对象则通过抽象接口来操作这些对象。

这种技术非常适用于数据驱动型应用程序。对于图形用户界面(GUI)应用程序,由于对象频繁地被创建和销毁,这种方法可能难以实施。为了验证依赖于抽象接口的类的正确行为,可以使用模拟(mocking)技术。例如,请参阅Googletest 模拟(gMock)框架。

将所有类编译为库

在中小型项目中,构建脚本通常会列出所有源文件,然后一次性编译出可执行文件。这意味着测试的构建脚本必须再次列出所需的源文件。

若在脚本中仅需列出一次源文件和头文件以构建静态库,操作会更为简便。随后,main() 函数将与该静态库进行链接以构建可执行文件,而测试程序则会与静态库进行链接。

对于在构建多个程序时使用相同源文件的项目,将共享类构建成动态链接库(或共享对象库)可能更为合适,这样每个程序(包括测试程序)都可以在运行时加载该库。 同样,将编译后的代码封装在库中,有助于避免在描述“应组合哪些组件来生成各种程序”时出现重复。

测试机的配置

以下各节将讨论因测试机设置不当而导致的常见问题:

通常,通过合理使用虚拟化技术,可以解决所有这些问题。

屏幕保护程序

屏幕保护程序可能会干扰某些GUI类的测试,导致测试结果不可靠。应禁用屏幕保护程序,以确保测试结果的一致性和可靠性。

系统对话框

操作系统或其他正在运行的应用程序意外显示的对话框可能会从参与自动测试的小部件上夺走输入焦点,从而导致无法重现的失败。

典型问题的示例包括 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

使用 NVIDIA 二进制驱动程序的用户请注意,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 。

或者,使用离屏插件:

TESTARGS="-platform offscreen" make check -k -j1

窗口管理器

在 Unix 系统上,至少有两个自动测试(tst_examples 和tst_gestures )需要运行窗口管理器。因此,如果在嵌套的 X 服务器下运行这些测试,您还必须在该 X 服务器中运行一个窗口管理器。

您的窗口管理器必须配置为自动在显示器上定位所有窗口。某些窗口管理器(例如 Tab Window Manager (twm))具有手动定位新窗口的模式,这会导致测试套件无法在无需用户交互的情况下运行。

注意:Tab Window Manager 不适合运行完整的 Qt 自动测试套件,因为tst_gestures 自动测试会导致其忘记配置并恢复为手动窗口定位模式。

© 2026 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.