警告

本节包含从C++自动翻译到Python的代码片段,可能包含错误。

Qt 测试概述

Qt单元测试框架概述。

Qt Test 是一个用于单元测试基于 Qt 的应用程序和库的框架。Qt Test 提供了单元测试框架中常见的所有功能,以及用于测试图形用户界面的扩展。

Qt Test 旨在简化基于 Qt 的应用程序和库的单元测试编写:

特性

详情

轻量级

Qt Test 由大约6000行代码和60个导出的符号组成。

自包含

Qt Test 只需要从 Qt Core 模块中获取少量符号即可进行非 GUI 测试。

快速测试

Qt Test 不需要特殊的测试运行器;不需要特殊的测试注册。

数据驱动测试

可以使用不同的测试数据多次执行测试。

基本GUI测试

Qt Test 提供了鼠标和键盘模拟的功能。

基准测试

Qt Test 支持基准测试,并提供了多种测量后端。

IDE友好

Qt Test输出的消息可以被Qt Creator、Visual Studio和KDevelop解释。

线程安全

错误报告是线程安全且原子性的。

类型安全

广泛使用模板可以防止由隐式类型转换引入的错误。

易于扩展

自定义类型可以轻松添加到测试数据和测试输出中。

您可以使用Qt Creator向导创建一个包含Qt测试的项目,并直接从Qt Creator中构建和运行它们。有关更多信息,请参阅Qt Creator:构建和运行测试。

创建一个测试

要创建一个测试,请子类化 QObject 并向其添加一个或多个私有槽。每个私有槽都是测试中的一个测试函数。qExec() 可用于执行测试对象中的所有测试函数。

此外,您可以定义以下不被视为测试函数的私有槽。当存在时,它们将由测试框架执行,并可用于初始化或清理整个测试或当前测试函数。

  • initTestCase() 将在第一个测试函数执行之前被调用。

  • initTestCase_data() 将被调用来创建一个全局测试数据表。

  • cleanupTestCase() 将在最后一个测试函数执行后被调用。

  • init() 将在每个测试函数执行之前被调用。

  • cleanup() 将在每个测试函数之后被调用。

使用initTestCase()来准备测试。每个测试都应该让系统处于可用状态,以便可以重复运行。清理操作应该在cleanupTestCase()中处理,这样即使测试失败,它们也会被执行。

使用 init() 来准备一个测试函数。每个测试函数都应使系统保持在可用状态,以便可以重复运行。清理操作应在 cleanup() 中处理,这样即使测试函数失败并提前退出,它们也会被执行。

或者,您可以使用RAII(资源获取即初始化),在析构函数中调用清理操作,以确保它们在测试函数返回且对象超出范围时发生。

如果 initTestCase() 失败,将不会执行任何测试函数。如果 init() 失败,接下来的测试函数将不会执行,测试将继续到下一个测试函数。

示例:

class MyFirstTest(QObject):

    Q_OBJECT
# private
    def myCondition():

        return True

# private slots
    def initTestCase():

        qDebug("Called before everything else.")

    def myFirstTest():

        QVERIFY(True) # check that a condition is satisfied
        QCOMPARE(1, 1) # compare two values

    def mySecondTest():

        QVERIFY(myCondition())
        QVERIFY(1 != 2)

    def cleanupTestCase():

        qDebug("Called after myFirstTest and mySecondTest.")

最后,如果测试类有一个静态公共的void initMain()方法,它会在QApplication对象实例化之前由QTEST_MAIN宏调用。这是在5.14版本中添加的。

更多示例,请参考Qt 测试教程

增加测试函数超时时间

QtTest 限制每个测试的运行时间,以捕获无限循环和类似的错误。默认情况下,任何测试函数调用将在五分钟后被中断。对于数据驱动的测试,这适用于每个具有不同数据标签的调用。可以通过设置 QTEST_FUNCTION_TIMEOUT 环境变量来配置此超时时间,该变量设置为单个调用可接受的最大毫秒数。如果测试花费的时间超过配置的超时时间,它将被中断,并调用 qFatal()。结果,测试默认情况下会中止,就像它崩溃了一样。

要在Linux或macOS上从命令行设置QTEST_FUNCTION_TIMEOUT,请输入:

QTEST_FUNCTION_TIMEOUT=900000
export QTEST_FUNCTION_TIMEOUT

在Windows上:

SET QTEST_FUNCTION_TIMEOUT=900000

然后在这个环境中运行测试。

或者,您可以在测试代码本身中以编程方式设置环境变量,例如通过从测试类的initMain()特殊方法调用:

qputenv("QTEST_FUNCTION_TIMEOUT", "900000");

要计算一个合适的超时值,请查看测试通常需要多长时间,并决定在没有出现某些问题的情况下,测试可以延长多长时间。将该延长时间转换为毫秒以获得超时值。例如,如果您决定一个通常需要几分钟的测试在慢速机器上可以合理地延长到二十分钟,那么将20 * 60 * 1000 = 1200000并将环境变量设置为1200000,而不是上面的900000

Qt 测试命令行参数

语法

执行自动测试的语法采用以下简单形式:

testname [options] [testfunctions[:testdata]]...

testname替换为您的可执行文件的名称。testfunctions可以包含要执行的测试函数的名称。如果没有传递testfunctions,则运行所有测试。如果您附加testdata中的条目名称,则测试函数将仅使用该测试数据运行。

例如:

/myTestDirectory$ testQString toUpper

使用所有可用的测试数据运行名为toUpper的测试函数。

/myTestDirectory$ testQString toUpper toInt:zero

使用所有可用的测试数据运行toUpper测试函数,并使用名为zero的测试数据行运行toInt测试函数(如果指定的测试数据不存在,相关的测试将失败并报告可用的数据标签)。

/myTestDirectory$ testMyWidget -vs -eventdelay 500

运行testMyWidget函数测试,输出每个信号发射,并在每个模拟的鼠标/键盘事件后等待500毫秒。

选项

日志选项

以下命令行选项决定了测试结果的报告方式:

  • -o filename,format 将输出写入指定的文件,使用指定的格式(可以是 txt, csv, junitxml, xml, lightxml, teamcitytap)。使用特殊文件名 -(连字符)将日志输出到标准输出。

  • -o filename 将输出写入指定的文件。

  • -txt 以纯文本形式输出结果。

  • -csv 将结果输出为逗号分隔值(CSV),适合导入电子表格。此模式仅适用于基准测试,因为它会抑制正常的通过/失败消息。

  • -junitxml 将结果输出为JUnit XML文档。

  • -xml 将结果输出为XML文档。

  • -lightxml 将结果输出为XML标签流。

  • -teamcity 以TeamCity格式输出结果。

  • -tap 以测试任何协议(TAP)格式输出结果。

-o 选项的第一个版本可以重复使用,以便以多种格式记录测试结果,但此选项的实例不能超过一个,用于将测试结果记录到标准输出。

如果使用了-o选项的第一个版本,则不应使用-o选项的第二个版本,也不应使用-txt-xml-lightxml-teamcity-junitxml-tap选项。

如果未使用-o选项的任何版本,测试结果将记录到标准输出。如果未使用格式选项,测试结果将以纯文本形式记录。

测试日志详情选项

以下命令行选项控制测试日志中报告的详细程度:

  • -silent 静默输出;仅显示致命错误、测试失败和最小状态消息。

  • -v1 详细输出;显示每个测试函数何时被调用。(此选项仅影响纯文本输出。)

  • -v2 扩展详细输出;显示每个 QCOMPARE()QVERIFY()。(此选项影响所有输出格式,并暗示纯文本输出的 -v1。)

  • -vs 显示所有发出的信号以及由这些信号引起的槽调用。(此选项影响所有输出格式。)

测试选项

以下命令行选项影响测试的运行方式:

  • -functions 输出测试中所有可用的测试函数,然后退出。

  • -datatags 输出测试中可用的所有数据标签。全局数据标签前面会加上‘ __global__ ’。

  • -eventdelay 毫秒 如果没有为键盘或鼠标模拟(keyClick()mouseClick() 等)指定延迟,则使用此参数的值(以毫秒为单位)。

  • -keydelay ms 类似于 -eventdelay,但仅影响键盘模拟,不影响鼠标模拟。

  • -mousedelay ms 类似于 -eventdelay,但仅影响鼠标模拟,不影响键盘模拟。

  • -maxwarnings number 设置输出的最大警告数量。0表示无限制,默认为2000。

  • -nocrashhandler 在Unix平台上禁用崩溃处理程序。在Windows上,它重新启用默认关闭的Windows错误报告对话框。这对于调试崩溃非常有用。

  • -repeat n 运行测试套件n次或直到测试失败。对于寻找不稳定的测试非常有用。如果为负数,则测试将无限重复。这旨在作为开发人员工具,并且仅支持纯文本记录器。

  • -skipblacklisted 跳过黑名单中的测试。此选项旨在通过防止黑名单中的测试夸大覆盖率统计数据,从而更准确地测量测试覆盖率。当不测量测试覆盖率时,建议执行黑名单中的测试,以揭示其结果中的任何变化,例如新的崩溃或导致黑名单的问题已解决。

  • -platform name 这个命令行参数适用于所有Qt应用程序,但在自动化测试的背景下可能特别有用。通过使用“offscreen”平台插件(-platform offscreen),可以运行使用QWidget或QWindow的测试,而不会在屏幕上显示任何内容。目前,offscreen平台插件仅在X11上得到完全支持。

基准测试选项

以下命令行选项控制基准测试:

  • -callgrind 使用 Callgrind 来计时基准测试(仅限 Linux)。

  • -tickcounter 使用CPU滴答计数器来计时基准测试。

  • -eventcounter 计算在基准测试期间接收到的事件数量。

  • -minimumvalue n 设置可接受的最小测量值。

  • -minimumtotal n 设置测试函数重复执行的最小可接受总数。

  • -iterations n 设置累积迭代的次数。

  • -median n 设置中位数迭代的次数。

  • -vb 输出详细的基准测试信息。

杂项选项

  • -help 输出可能的命令行参数并提供一些有用的帮助。

Qt 测试环境变量

您可以设置某些环境变量以影响自动测试的执行:

  • QTEST_DISABLE_CORE_DUMP 将此变量设置为非零值将禁用核心转储文件的生成。

  • QTEST_DISABLE_STACK_DUMP 将此变量设置为非零值将防止Qt Test在自动测试超时或崩溃时打印堆栈跟踪。

  • QTEST_FATAL_FAIL 将此变量设置为非零值将导致自动测试中的失败立即中止整个自动测试。这对于调试测试中的不稳定或间歇性失败非常有用,例如通过在调试器中启动测试。此变量的支持在 Qt 6.1 中添加。

  • QTEST_THROW_ON_FAIL (自6.8版本起) 将此变量设置为非零值将导致QCOMPARE() / QVERIFY()等在失败时抛出异常(而不是仅仅从直接包围的函数范围返回)。

  • QTEST_THROW_ON_SKIP (自6.8版本起) 与 QTEST_THROW_ON_FAIL 相同,但影响 QSKIP()

创建基准

要创建一个基准测试,请按照创建测试的说明操作,然后在你想要进行基准测试的测试函数中添加QBENCHMARK宏或setBenchmarkResult()。在以下代码片段中,使用了宏:

class MyFirstBenchmark(QObject):

    Q_OBJECT
# private slots
    def myFirstBenchmark():

        string1 = QString()
        string2 = QString()
        QBENCHMARK {
            string1.localeAwareCompare(string2)

一个用于测量性能的测试函数应包含一个QBENCHMARK宏或一个对setBenchmarkResult()的调用。多次出现没有意义,因为每个测试函数或数据驱动设置中的每个数据标签只能报告一个性能结果。

避免更改构成(或影响)QBENCHMARK宏主体的测试代码,或计算传递给setBenchmarkResult()的值的测试代码。理想情况下,连续性能结果的差异应仅由您正在测试的产品的更改引起。对测试代码的更改可能会导致性能变化的误导性报告。如果您确实需要更改测试代码,请在提交消息中明确说明。

在性能测试函数中,QBENCHMARKsetBenchmarkResult() 之后应该跟随一个验证步骤,使用 QCOMPARE()QVERIFY() 等。如果测量的是非预期的代码路径,您可以将性能结果标记为无效。性能分析工具可以使用此信息来过滤掉无效结果。例如,意外的错误条件通常会导致程序过早地从正常程序执行中退出,从而错误地显示出显著的性能提升。

选择测量后端

QBENCHMARK宏内的代码将被测量,并且可能会重复几次以获得准确的测量结果。这取决于所选的测量后端。有几种后端可用。它们可以在命令行中选择:

名称

命令行参数

可用性

Walltime

(默认)

所有平台

CPU 滴答计数器

-tickcounter

Windows, macOS, Linux, 许多类 UNIX 系统。

事件计数器

-eventcounter

所有平台

Valgrind Callgrind

-callgrind

Linux(如果已安装)

Linux 性能分析工具

-perf

Linux

简而言之,walltime 总是可用的,但需要多次重复才能获得有用的结果。Tick 计数器通常可用,并且可以用较少的重复次数提供结果,但可能容易受到 CPU 频率缩放问题的影响。Valgrind 提供精确的结果,但不考虑 I/O 等待,并且仅在有限数量的平台上可用。事件计数在所有平台上都可用,它提供了事件循环在发送到其相应目标之前接收到的事件数量(这可能包括非 Qt 事件)。

Linux性能监控解决方案仅在Linux上可用,并提供许多不同的计数器,可以通过传递一个额外的选项-perfcounter countername来选择,例如-perfcounter cache-misses-perfcounter branch-misses-perfcounter l1d-load-misses。默认计数器是cpu-cycles。可以通过运行任何带有选项-perfcounterlist的基准测试可执行文件来获取完整的计数器列表。

  • 使用性能计数器可能需要启用对非特权应用程序的访问。

  • 不支持高分辨率计时器的设备默认为一毫秒的粒度。

请参阅Qt测试教程中的编写基准测试以获取更多基准测试示例。

使用全局测试数据

你可以定义initTestCase_data()来设置一个全局测试数据表。每个测试都会为全局测试数据表中的每一行运行一次。当测试函数本身是数据驱动的时,它会为每个本地数据行运行,为每个全局数据行运行。因此,如果全局数据表中有g行,测试自己的数据表中有d行,那么这个测试的运行次数是g乘以d

全局数据使用QFETCH_GLOBAL()宏从表中获取。

以下是全局测试数据的典型用例:

  • 在QSql测试中选择可用的数据库后端,以便对每个数据库运行每个测试。

  • 进行所有网络测试,包括使用和不使用SSL(HTTP与HTTPS)以及代理。

  • 使用高精度时钟和粗略时钟测试计时器。

  • 选择解析器是否应从QByteArray或QIODevice读取。

例如,使用initTestCase_data()提供的每个区域设置来测试roundTripInt_data()提供的每个数字:

def roundTripInt(self):

    QFETCH_GLOBAL(QLocale, locale)
    QFETCH(int, number)
    ok = bool()
    QCOMPARE(locale.toInt(locale.toString(number), ok), number)
    QVERIFY(ok)

在测试的命令行中,你可以传递一个函数的名称(不带测试类名前缀)以仅运行该函数的测试。如果测试类具有全局数据,或者函数是数据驱动的,你可以在冒号后附加一个数据标签,以仅运行该标签的数据集用于该函数。要同时指定全局标签和特定于测试函数的标签,请将它们用冒号组合在一起,将全局数据标签放在前面。例如

./testqlocale roundTripInt:zero

将运行上述roundTripInt()测试的zero测试用例(假设其TestQLocale类已编译为可执行的testqlocale),在initTestCase_data()指定的每个区域设置中运行,同时

./testqlocale roundTripInt:C

将仅在C语言环境中运行roundTripInt()的所有三个测试用例,并且

./testqlocale roundTripInt:C:zero

将仅在C语言环境中运行zero测试用例。

提供如此细粒度的控制来运行哪些测试可以显著简化问题的调试过程,因为你只需要逐步检查那个被发现失败的测试用例。