警告

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

Qt测试概述#

Qt单元测试框架概述。

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

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

特性

详细说明

轻量级

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

自包含

Qt Test对于非GUI测试,只需要从Qt Core模块中获取少量符号。

快速测试

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

数据驱动测试

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

基本的GUI测试

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

基准测试

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

对IDE友好

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

线程安全性

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

类型安全性

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

易于扩展

可以轻松地将自定义类型添加到测试数据中。

您可以使用Qt Creator向导创建包含Qt测试的项目,并通过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 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 乘以,并将环境变量设置为 1200000 而不是上面的 900000

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 如果没有为键盘或鼠标模拟指定延迟(如 keyClick()mouseClick() 等),则此参数的值(以毫秒为单位)将被替换。

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

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

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

  • -nocrashhandler 在 Unix 平台上禁用崩溃处理器。在 Windows 上,它重新启用 Windows 错误报告对话框(默认情况下关闭)。这在调试崩溃时很有用。

  • -repeat n 运行测试套件 n 次或直到测试失败。对于寻找不可靠的测试非常有用。如果是负数,则测试将持续重复。这是一个开发工具,并且仅支持纯文本记录器。

  • -skipblacklisted 跳过黑名单中的测试。此选项旨在通过防止黑名单中的测试增加覆盖率统计来使得测试覆盖率的测量更精确。如果不测量测试覆盖率,建议执行黑名单中的测试以揭示其结果的任何更改,例如新的崩溃或者是触发黑名单的問題得到解决。

  • -platform 名称 这个命令行参数适用于所有Qt应用程序,但在自动测试的上下文中可能特别有用。通过使用“离屏”平台插件 (-platform offscreen),可以在屏幕上不显示任何内容的情况下运行QWidget或QWindow的测试。目前,离屏平台插件仅在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中。

创建基准测试#

要创建基准测试,遵循创建测试的说明,然后为要基准测试的测试函数添加一个 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

-perf

Linux

简而言之,墙时总是可用,但需要多次重复才能得到有用的结果。时钟计数器通常可用,并可以使用较少的重复提供结果,但可能受CPU频率调整问题的影响。Valgrind提供精确结果,但未将I/O等待考虑在内,并且仅在有限数量的平台上可用。事件计数在所有平台上可用,并提供了在将事件发送到它们的相应目标之前接收到的events的数量(这可能包括非Qt事件)。

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

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

  • 不支持高精度定时器的设备默认为每毫秒粒度。

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

使用全局测试数据#

您可以使用 initTestCase_data() 函数来设置全局测试数据表。每个测试都会针对全局测试数据表中的每一行运行一次。当测试函数本身是数据驱动的,它会针对每一行本地数据和每一行全局数据进行运行。因此,如果在全局数据表中存在 g 行,在测试本身的数据表中存在 d 行,则此测试的运行次数为 gd

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

以下是一些典型使用全局测试数据的场景

  • 在 QSql 测试中选择可用的数据库后端,针对每种数据库运行所有测试。

  • 在带和不带 SSL(HTTP 与 HTTPS)以及代理的情况下进行所有网络测试。

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

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

例如,要测试 roundTripInt_data() 提供的每个数字以及 initTestCase_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 测试用例。

在运行测试方面的这种细粒度控制可以使调试问题变得容易得多,因为您只需逐步通过已显示失败的单个测试用例。