Qt Test 概览

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 中构建和运行它们。更多信息,请参阅 运行自动测试

创建测试

要创建测试,从 QObject 派生,并向其添加一个或多个私有槽。每个私有槽都是测试函数。可以使用 QTest::qExec() 来执行测试对象中的所有测试函数。

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

  • initTestCase() 将在第一个测试函数执行之前调用。
  • initTestCase_data() 将被调用以创建全局测试数据表。
  • cleanupTestCase() 将在最后一个测试函数执行之后调用。
  • init() 将在每次测试函数执行之前调用。
  • cleanup() 将在每次测试函数执行之后调用。

使用 initTestCase() 来准备测试。每个测试都应该将系统置于可用的状态,以便可以重复运行。清理操作应在 cleanupTestCase() 中处理,以使即使在测试失败的情况下也能运行。

使用 init() 来准备测试函数。每个测试函数都应该将系统置于可用的状态,以便可以重复运行。清理操作应在 cleanup() 中处理,以使即使在测试函数失败和提前退出时也能运行。

另外,您可以使用RAII(资源获取即初始化),在析构函数中调用清理操作,以确保它们在测试函数返回并且对象离开作用域时发生。

如果initTestCase()失败,则不会执行任何测试函数。如果init()失败,则不会执行以下测试函数,测试将跳过并继续下一个测试函数。

示例

class MyFirstTest: public QObject
{
    Q_OBJECT

private:
    bool myCondition()
    {
        return true;
    }

private slots:
    void initTestCase()
    {
        qDebug("Called before everything else.");
    }

    void myFirstTest()
    {
        QVERIFY(true); // check that a condition is satisfied
        QCOMPARE(1, 1); // compare two values
    }

    void mySecondTest()
    {
        QVERIFY(myCondition());
        QVERIFY(1 != 2);
    }

    void cleanupTestCase()
    {
        qDebug("Called after myFirstTest and mySecondTest.");
    }
};

最后,如果测试类有一个静态公有的void initMain()方法,它将在实例化QTEST_MAIN宏之前被调用。该功能自5.14版开始提供。

有关更多示例,请参阅Qt Test教程

增加测试函数超时时间

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并与上面的900000进行比较,并将环境变量设置为1200000

构建一个测试

您可以构建一个包含一个测试类的可执行文件,该测试类通常用于测试一种生产代码类。但是,通常您想要通过运行单个命令来在一次运行中测试项目中的几个类。

请参阅编写单元测试以了解分步说明。

使用CMake和CTest进行构建

您可以使用使用CMake和CTest进行构建来创建测试。CTest允许您根据与测试名称匹配的正则表达式包含或排除测试。您可以进一步将LABELS属性应用于测试,并CTest可以根据这些标签包含或排除测试。当在命令行上调用代码toggle_idtest目标时,所有带标签的目标将会被执行。

注意:在Android上,如果您只有一个连接的设备或模拟器,测试将在该设备上运行。如果您有多个连接的设备,请设置环境变量ANDROID_DEVICE_SERIAL为您想要在该设备上运行测试的ADB序列号

使用CMake还有其他几个优势。例如,使用CDash几乎无需任何努力即可将测试运行的结果发布到Web服务器上。

CTest可以扩展到非常不同的单元测试框架,并且与QTest配合使用。

以下是一个CMakeLists.txt文件示例,该文件指定了项目名称和使用语言(在这里是mytest和C++),用于构建测试所需的Qt模块(Qt5Test),以及包括在测试中的文件(tst_mytest.cpp)。

project(mytest LANGUAGES CXX)

find_package(Qt6 REQUIRED COMPONENTS Test)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOMOC ON)

enable_testing(true)

qt_add_executable(mytest tst_mytest.cpp)
add_test(NAME mytest COMMAND mytest)

target_link_libraries(mytest PRIVATE Qt::Test)

有关您可以选择的选项的更多信息,请参见使用CMake进行构建

使用qmake构建

如果您使用qmake作为构建工具,只需将以下内容添加到您的项目文件中

QT += testlib

如果您想通过make check运行测试,请添加额外的行

CONFIG += testcase

为了防止测试被安装到您的目标中,请添加额外的行

CONFIG += no_testcase_installs

有关make check的更多信息,请参阅qmake手册

使用其他工具构建

如果您正在使用其他构建工具,请确保将Qt Test头文件的路径添加到您的包含路径中(通常是位于您的Qt安装目录下的include/QtTest)。如果您正在使用Qt的发布版构建,请将您的测试链接到QtTest库。对于调试构建,请使用QtTest_debug

Qt Test命令行参数

语法

执行自动化测试的语法如下所示

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
    将输出写入指定的文件,格式如下(txtcsvjunitxmlxmllightxmlteamcitytap 之一)。使用特殊文件名 -(连字符)将日志记录到标准输出。
  • -o filename
    将输出写入指定的文件。
  • -txt
    以纯文本格式输出结果。
  • -csv
    以逗号分隔值(CSV)格式输出结果,适用于导入电子表格。此模式仅适用于基准测试,因为该模式会抑制正常的通过/失败信息。
  • -junitxml
    以JUnit XML文档形式输出结果。
  • -xml
    以XML文档形式输出结果。
  • -lightxml
    以XML标签流的形式输出结果。
  • -teamcity
    TeamCity格式输出结果。
  • -tap
    Test Anything Protocol (TAP) 格式输出结果。

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

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

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

测试日志明细选项

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

  • -silent
    静默输出;仅显示致命错误、测试失败和最基本的状态消息。
  • -v1
    详细输出;显示每个测试函数何时被调用。(此选项仅影响纯文本输出。)
  • -v2
    扩展详细输出;显示每个QCOMPARE()和QVERIFY。(此选项影响所有输出格式并暗示纯文本输出使用-v1。)
  • -vs
    显示所有发出的信号及其由此信号引发的槽调用。(此选项影响所有输出格式。)

测试选项

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

  • -functions
    输出测试中可用的所有测试函数,然后退出。
  • -datatags
    输出测试中可用的所有数据标签。全局数据标签之前有一个' __global__ '。
  • -eventdelay ms
    如果未指定键盘或鼠标模拟的延迟(如QTest::keyClickQTest::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),可以使使用 QWidgetQWindow 运行的测试在没有显示任何内容到屏幕上的情况下执行。目前,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 测试在自动测试超时或崩溃时打印堆栈跟踪。
  • QTEST_FATAL_FAIL
    将此变量设置为非零值将在自动测试失败时立即终止整个自动测试。这在例如调试测试中不稳定或不连续的失败时非常有用,可以通过在调试器中启动测试来完成。此变量的支持是在 Qt 6.1 中添加的。

创建基准测试

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

class MyFirstBenchmark: public QObject
{
    Q_OBJECT
private slots:
    void myFirstBenchmark()
    {
        QString string1;
        QString string2;
        QBENCHMARK {
            string1.localeAwareCompare(string2);
        }
    }
};

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

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

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

选择测量后端

QBENCHMARK宏内部的代码将被测量,也许还需要重复几次以获得准确的测量值。这取决于所选的测量后端。提供几种不同的后端可以选择。它们可以在命令行上选择

名称命令行参数可用性
系统时间(默认)所有平台
CPU滴答计时器-tickcounterWindows、macOS、Linux、许多类似UNIX的系统。
事件计数器-eventcounter所有平台
Valgrind Callgrind-callgrindLinux(如果已安装)
Linux Perf-perfLinux

简而言之,表面时间始终可用,但需要多次重复才能获得有用结果。滴答计数器通常可用,并且可以提供较少重复的结果,但可能对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和不带SSL(HTTP与HTTPS)的以及代理网络测试。
  • 用高精度时钟和粗时钟对计时器进行测试。
  • 选择解析器是否从QByteArray还是从QIODevice读取。

例如,要测试由 roundTripInt_data() 提供的每个数字和由 initTestCase_data() 提供的每个区域设置。

void TestQLocale::roundTripInt()
{
    QFETCH_GLOBAL(QLocale, locale);
    QFETCH(int, number);
    bool ok;
    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 测试用例。

通过这样精细地控制要运行的测试,可以显着简化问题的调试,因为您只需遍历已看到失败的测试用例。

© 2024 Qt 公司有限公司。本体内的文档贡献是各自所有者的版权。本体内提供的文档已在自由软件基金会发布的 GNU 自由文档许可证版本 1.3 的条款下授权。Qt 和相应的标志是芬兰的 Qt 公司及/或全球其他国家的 商标。所有其他商标均为各自所有者的财产。