《OpenCV3和Qt5计算机视觉应用开发》学习笔记第10章

自从以OpenCV3与Qt5框架开启计算机视觉之旅以来,我们已经取得了长足的进步。我们现在可以很容易地安装这些强大的框架,并配置运行Windows、macOS或Linux操作系统的计算机,以设计和构建计算机视觉应用程序。在前几章中,我们学习了如何使用Qt插件系统来构建模块化和基于插件的应用程序。我们学习了如何使用Qt样式表对应用程序进行样式化,并使用Qt中的国际化技术支持多种语言。利用Qt图形视图框架构建了功能强大的图形查看器应用程序,Qt图形视图框架中的类能够帮助我们更高效和灵活地显示图形对象元素。我们能够构建图形查看器(得益于场景-视图-对象元素架构),它可以在不需处理源图像本身的情况下对图像进行放大或缩小操作。随后,我们开始深入研究OpenCV框架,学习该框架下的许多类和函数,这些类和函数能够帮助我们以多种方式对图像进行转换和处理,以实现特定的计算机视觉目标。我们学习了可以用来在场景中检测对象的特征检测和描述符提取。我们讨论了OpenCV中的许多现有算法,这些算法的目的是以一种更智能的方式对图像内容进行处理,而不仅仅是对它们的原始像素值进行处理。在最近的几章中,我们学习了Qt提供的多线程和线程同步工具,学习了Qt框架提供的与平台无关的用于处理应用程序中的多线程的底层(QThread)和高级(QtConcurrent)技术。在最后一章,我们学习了视频的实时图像处理以及用于跟踪特定颜色对象的OpenCV算法。至此,我们对Qt和OpenCV框架诸多方面有了深入的了解,在此基础上并借助于文档,我们已经可以独立学习其他更高级的主题。
除了前面提到的内容以及在前几章中所学习到的内容之外,我们还没有介绍软件开发的一个非常重要的内容——测试过程,以及如何在使用Qt和OpenCV时进行测试。一个计算机程序,无论是简单的小型二进制文件,还是庞大的计算机视觉应用程序,或者是任何一般的应用程序,在交付用户部署使用之前,都要进行测试。测试是开发过程中一个永无止境的阶段,并且要在开发应用程序之后随即执行,在解决了一个问题或添加新特性时,也要不时地进行测试。本章中,我们将学习用现有的技术对基于Qt和OpenCV构建的应用程序进行测试。我们将学习开发时的测试与调试,还将学习如何使用Qt测试框架对应用程序进行单元测试。在将应用程序交付给最终用户之前,这是最重要的过程。


本章将介绍以下主题:
❑ Qt Creator的调试特性
❑ 如何使用Qt Test Namespace进行单元测试
❑ 数据驱动的测试
❑ GUI测试以及回放GUI事件
❑ testcase工程项目的创建

10.1 Qt Creator调试

调试器是一种可用于测试和调试其他程序的程序,目的是防止程序执行过程中突然崩溃或程序逻辑中的意外行为。大多数情况下(如果不总是这样),调试器需要在开发环境中与IDE结合使用。在这里,我们将学习如何在Qt Creator中使用调试器。需要注意的是,调试器不是Qt框架的一部分,而且与编译器一样,通常它们是由操作系统SDK提供的。如果在系统上存在调试器,Qt Creator可以自动检测并使用这些调试器。这可以通过选择主菜单“Tools”,然后选择“Options”并导航到Qt Creator选项页面进行勾选。确保从左侧的列表中选择“Build&Run”,然后从顶部切换到“Debuggers”选项卡。此时,窗口在列表中应该可以看到一个或多个自动检测到的调试器。

Windows用户:应该可以看到与图10-1类似的窗口。没有的话,就表示还没有安装任何调试器。你可以轻松地使用这里提供的地址下载和安装它:

Install WinDbg - Windows drivers | Microsoft Learn

也可以在网上单独搜索以下主题:Windows调试工具(WinDbg、KD、CDB、NTSD)。尽管如此,在安装调试器之后(假设是Microsoft Visual C++编译器的CDB或Microsoft控制台调试器以及GCC编译器的GDB等),可以重启Qt Creator,并返回图10-1所示的窗口。你应该能够有一个或多个与下面内容类似的项。因为已经安装了Qt的32位版本和OpenCV框架,请选择名称中含有x86的项来查看其路径、类型以及其他一些属性。

macOS和Linux用户:

不必进行任何其他的操作,根据操作系统的不同,将可以看到GDB、LLDB或其他一些调试器。

图10-1是“Options”页面上的“Build&Run”选项卡的截图。

                                     图10-1 “Options”页面上“Build&Run”选项卡截图

取决于操作系统和安装的调试器,图10-1选项卡的截图可能略有不同。不过,需要确保将一个已有的调试器正确地设置为正在使用的Qt工具包的调试器。因此,请记录下这个调试器的路径和名称,并切换到“Kits”选项卡,然后选择正在使用的Qt工具包,确保已为其设置正确的调试器,如图10-2所示。

                                                图10-2 “Kits”选项卡界面截图

不用担心会选错调试器或其他一些选项,因为在顶部选择的Qt工具包图标旁会有相关图标发出警告。当工具箱的一切都正常的时候,会看到图10-3中左边所示的图标,而当左边第二个图标出现时,就表示有些内容是不正确的。当出现最右边的图标时,表示存在严重错误。将鼠标移动到相应的图标上,可看到有关修复问题所需操作的更多相关信息:

                                                          图10-3 相关图标截图

如果Qt工具包出现严重问题,则可能是很多不同原因造成的,比如缺少编译器,在问题得到解决之前,会使工具包完全无法使用。Qt工具包中出现警告信息的一个例子也可能是缺少调试器,此时工具包可用,但仍将无法使用调试器。因此,这意味着比完全配置的Qt工具包的功能更少。

在正确设置调试器之后,可以以下列方式之一进入Qt Creator的调试器视图,开始调试应用程序:
❑ 在调试模式下启动应用程序
❑ 连接到正在运行的应用程序(或进程)

请注意,可通过多种方式(比如远程)来启动调试过程,方法是连接到一台独立的机器上运行的进程,等等。但是,上述方法适用于大多数情况,特别是对于那些与Qt+OpenCV应用程序开发以及在本书中学到的知识相关的情况。

调试模式入门
在打开Qt工程项目之后,若要在调试模式中启动应用程序,可以使用下列方法之一:
❑ 按下F5键
❑ 使用“Start Debugging”按钮,它是常规的“Run”按钮下方一个类似的图标,但上面有一只小虫子
❑ 按以下顺序使用主菜单项:Debug/Start Debugging/Start Debugging
要将调试器连接到正在运行的应用程序,可以按照以下顺序使用主菜单项:Debug/Start Debugging/Attach to Running Application。此时将打开“List of Processes”窗口,可以从中选择想要使用其进程ID或可执行文件名称进行调试的应用程序或任何其他进程。进程列表可能会很长,还可以使用“Filter”字段(如图10-4所示)查找应用程序。选择正确的进程后,确保按下“Attach to Process”按钮。

                                      图10-4 “List of Processes”窗口的界面截图

无论你使用哪一种方法,最终都将进入Qt Creator调试模式,调试模式与编辑模式非常相似,但它还允许执行以下操作:
❑ 在代码中添加、启用、禁用以及查看断点(断点只是在代码中的一个点或行,我们希望调试器在进程中暂停在这里,并对程序状态进行更详细的分析)
❑ 中断正在运行的程序和进程来查看和检查代码
❑ 查看和检查函数的调用堆栈(调用堆栈是一个包含产生断点或中断状态的函数的层次列表的堆栈)
❑ 查看和检查变量
❑ 反汇编源代码(这里,反汇编是指提取与函数调用以及程序中其他C++代码相对应的精确指令)

在调试模式下启动应用程序时,因为代码被调试器监测和跟踪,将导致性能有所下降。图10-5是Qt Creator调试模式的屏幕截图,前面介绍的所有功能都可以在一个窗口以及Qt Creator的调试模式中看到。

                                     图10-5 Qt Creator调试模式的屏幕截图

图10-5的截图中标有数字1的区域为代码编辑器,你已经使用过,应该非常熟悉了。每一行代码都有一个行号,单击代码行的左侧可在代码中任意位置设置或取消一个断点。还可以右键单击要设置、删除、禁用或启用断点的行号,方法是选择“Set Breakpoint at Line X”“Remove Breakpoint X”“Disable Breakpoint X”或“Enable Breakpoint X”命令,其中需要将X替换为相应的行号。除了代码编辑器之外,还可以使用图10-5中标有数字4的区域来添加、删除、编辑和进一步修改代码中的断点。

在代码中设置了断点之后,无论何时程序到达代码中的该行,程序都将中断。此时,将允许使用代码编辑器下面的控件执行下列任务:
❑ Continue:继续执行该程序的剩余部分(或者再次按F5)。
❑ StepOver:用于在不进入可能会改变调试指针当前位置的函数调用或者类似代码的情况下,执行下一代码行。注意,调试指针只是当前正在执行的代码行的指示器(这也可以通过按F10来完成)。
❑ Step Into:与Step Over相反,可以用来进一步深入到函数调用中,以便能够对代码和调试进行更详细的分析(这与按下F11是一样的)。
❑ Step Out:可以用来退出函数调用,返回到调试时的调用点(与按下Shift+F11是一样的)。

                图10-6 调试器控件菜单项界面截图

还可以在包含调试器控件的代码编辑器下面的同一个工具栏上右键单击,打开图10-6所示的菜单,并添加或删除更多的窗体,来显示额外的调试与分析信息。我们将介绍默认的调试器视图,但是,你必须查看下列每一个选项,以增加对调试器的熟悉程度。

在图10-5中,数字2所指示的区域可以用来查看调用堆栈。不管是在程序正在运行时通过按下“Interrupt”按钮或从菜单中选择“Debug/Interrupt”来中断程序,还是在一个特定的代码行设置断点并停止程序,或者是一段出现故障的代码导致程序落入陷阱并使进程暂停(因为调试器将捕获崩溃和异常),总是可以随时查看导致中断状态的函数调用的层次结构,或者通过检查图10-5的Qt Creator屏幕截图中的区域2进行进一步分析。

最后,可以借助图10-5中的第三个区域,在被中断的代码位置中查看程序的局部和全局变量。可以看到变量的内容,不管它们是否是标准数据类型(如整数、浮点数、结构体或者类),还可以进一步扩展和分析其内容来测试和分析代码中任何可能存在的问题。

高效地使用调试器可以提高测试以及解决代码问题的效率。对调试器的使用,没有其他捷径,只能尽可能多地使用调试器并培养良好的习惯,熟能生巧。如果感兴趣的话,还可以在网上查阅其他可能的调试方法,例如远程调试、基于崩溃转储文件(在Windows上)进行调试等等。

10.2 Qt测试框架

应用程序开发过程中对应用程序的调试与测试是完全不可避免的,但是很多开发人员往往忽略单元测试。单元测试很重要,尤其是在大型项目和应用程序中,每次在建立这些项目和程序时,或者在代码中的某个地方存在bug时,很难完全手动对其进行全面测试。单元测试是指对应用程序的控件和部分(单元)进行测试,以确保它们能够按预期进行工作。值得注意的是,自动化测试是当今软件开发的热点之一,其实质是使用第三方软件或者编程进行自动单元测试。

在本节中,将学习如何使用Qt测试框架(更准确地说,即Qt测试命名空间,连同一些附加的测试相关类),它可以用来为用Qt构建的应用程序开发单元测试。与第三方测试框架相反,Qt测试框架是基于Qt框架本身的轻量级测试框架,提供了基准测试、数据驱动测试和GUI测试等多种功能。基准测试可以用来测量一个函数或一段特定代码段的性能,而数据驱动的测试可以使用不同的数据集作为输入来运行单元测试。另一方面,通过模拟鼠标和键盘交互,可以进行GUI测试,这是Qt测试框架包含的另一个内容。

10.2.1 创建单元测试

若要创建单元测试,可以子类化QObject类,并向其中添加Qt测试框架所需的槽,以及一个或多个用于执行各种测试的槽(测试函数)。除了测试函数之外,在每个测试类中可以存在下面的槽(私有槽),并由Qt Test调用:
❑ initTestCase:在调用第一个测试函数之前调用。若该函数失败,则整个测试将失败,并且不会再调用测试函数。
❑ cleanupTestCase:在调用最后一个测试函数之后调用。
❑ init:在调用每个测试函数之前调用。如果该函数失败,将不再执行前面的测试函数。
❑ cleanup:在调用每个测试函数之后调用。

下面借助一个真实的例子来创建我们的第一个单元测试,并深入了解如何将上述函数添加到一个测试类中,以及如何编写测试函数。为了确保示例简单易行且易于理解,将尽量避免过多地考虑待测试类的实现细节,而主要关注如何对它们进行测试。基本上,同样的方法可用于测试任何复杂级别的几乎任何类。

因此,作为第一个例子,假设一个类能够返回图像中像素的数量(图像宽度乘以高度),而我们想要对该类进行单元测试。

1.利用Qt Creator创建一个单元测试,类似于创建Qt应用程序或库,在欢迎模式中使用“New Project”按钮或从“File”菜单中选择“New File or Project”,确保选择如图10-7所示的选项作为工程项目模板。

                              图10-7 在New Project界面下创建单元测试选项界面截图

2.单击“Choose”并输入“HelloTest”作为单元测试的项目名称,然后单击“Next”。
3.与Qt项目完全一样,选择工具包“Kit”,然后再次单击“Next”。
4.在图10-8中的Modules页面中,你会注意到QtCore和QtTest模块是默认选中的,不能取消。该页面只是一个助手或者说是向导,帮助你交互式地选择所需的模块。如果忘记添加类正确工作所需要的模块,还可以使用工程项目*.pro文件来添加或移除模块。这里需要强调一点,单元测试其实就像一个使用类和函数的应用程序。唯一的区别在于只需用它来进行测试,它的存在只是为了确保事情按预期正常工作,而且不会有返工。

                                            图10-8 单元测试模块选项界面截图

5.选择模块后单击“Next”,将出现“Details”页面或“Test Class Information”页面。在图10-9所示的“Test Slot”字段中输入“testPixelCount”,然后单击“Next”。剩下的选项与前面的窗口一样,可以很容易交互式地添加所需的功能,并将指令包括到测试单元中,也可以随后在源文件中予以添加。尽管如此,将在本章后面对它们的含义及其用法进行学习。

                                                图10-9 Test Class Information页面截图

6.在确认所有对话框之后,最后将进入Qt Creator编辑模式下的代码编辑器。检查HelloTest.pro文件,会发现它与标准Qt工程项目(控件或控制台应用程序)的*.pro文件非常相似,并包含下面的模块定义,用于将Qt测试模块导入到工程项目中。这就是在任意一个单元测试项目中使用Qt测试的方法。但是,如果不使用“New File or Project”向导,那么就将会自动添加下面的代码:

QT += testlib

在进入下一步之前,确保将OpenCV库添加到pro文件中,就像在Qt控件应用程序中所做的那样(有关该内容,请参考本书的前几章)。

7.现在,将前面创建的用于对图像的像素计数的类,添加到该项目中。注意,在这种情况下,添加和复制不是一回事。可以将一个单独文件夹中属于另一个项目的类头和源文件添加到项目中,而不需将其复制到项目文件夹下。只需确保在*.pro文件的HEADERS和SOURCES列表中包含它们,并且,还可以选择将类所在的文件夹添加到INCLUDEPATH变量中。

实际上,永远不应将正在测试的类的源文件复制到测试项目中,该内容将在本节中进一步讨论。为了至少在项目中添加一个单元测试,并且每次在构建主项目时能够自动执行测试,应该用subdirs模板创建工程项目,即使只包含一个项目。但是,严格地说,无论是将类的文件复制到单元测试中,还是不复制添加它们,单元测试都将以相同的方式工作。

8.现在开始编写测试类,请在Qt Creator代码编辑器中打开tst_hellotesttest.cpp文件。除了明显的#include指令之外,还有一些需要注意的事项:一个是HelloTestTest类,这是在“New File or Project”向导中提供的类名,它只不过是一个QObject子类,所以不要在此处查找任何隐藏的内容。它有一个名为testPixelCount的私有槽,也是在向导期间设置的,其实现包括一个带有QVERIFY2宏的单行代码,我们将在后面的步骤中进一步介绍。但是,最后两行是新添加的,如下所示:

QTEST_APPLESS_MAIN(HelloTestTest)
#include "tst_hellotesttest.moc"

QTEST_APPLESS_MAIN是一个宏,将由C++编译器和moc(有关moc的更多信息,请参见第3章)展开,用以创建一个恰当的C++main函数,来执行在HelloTest-Test类中编写的测试函数。它仅仅创建测试类的实例,并调用QTest::qExec函数来启动测试进程。测试进程中将会自动调用测试类中的所有私有槽,并输出测试结果。最后,当在单个cpp源文件中,而不是在单独的头文件和源文件中,创建测试类时,Qt框架需要上面的第二行代码。请确保使用include指令将待测试的类添加至tst_hellotesttest.cpp文件中(为易于查找,假设将其命名为PixelCounter)。

9.现在,可以使用其中一个合适的测试宏来测试该类中负责对图像像素计数的函数。假设它是一个输入文件名和路径(QString类型)并返回整数的函数。让我们在testPixelCount槽内使用现有的VERIFY2宏,如下所示:

void HelloTestTest::testPixelCount()
{
    int width = 688;
    int height = 793;
    QString fname = "E:/Image/aa.png";
    PixelCounter c;
    QVERIFY2(c.countPixels(fname) == width * height, "Failure");
}

在该测试中,我们提供了一个已知像素计数(宽度乘以高度)的图像文件,来测试函数是否能够正常工作。然后,创建PixelCounter类的实例,并最终执行QVERIFY2宏,它将执行countPixels函数(假设这是想测试的公有函数的名称),并基于比较的结果决定测试失败还是通过。测试失败的情况下,还会输出“Failure”字符串。

我们刚刚建立了第一个单元测试项目。单击Run按钮运行此测试并在Qt Creator输出窗体中查看结果。如果测试通过,那么将看到与下面类似的内容:

如果失败,将输出以下内容:

结果不言自明,但有一点需要注意,即在所有测试函数之前都调用了initTestCase,并且在所有的测试函数之后都调用了cleanupTestCase,这与前文的介绍一致。然而,由于这些函数实际上并不存在,因此只是将它们标记为PASS。如果实现了这些函数并进行了真正的初始化和终结任务,那么情况就会有所不同。

前面的示例介绍了单元测试最简单的形式,但是在实际中,编写一个能够处理所有可能问题的高效且可靠的单元测试,是一项艰巨的任务,与我们所遇到的问题相比要复杂得多。为了能够编写适当的单元测试,可以在每一个测试函数中使用下面的宏。这些宏是在QTest中定义的,如下所示:

❑QVERIFY:可用于检查是否满足一个条件。条件只是一个布尔值或任何计算值为布尔值的表达式。如果条件未满足,则测试停止、失败,并在输出中记录。否则,测试将继续。
❑QTRY_VERIFY_WITH_TIMEOUT:与QVERIFY类似,但是该函数试图检查给定的条件,要么直到达到指定的超时(以毫秒为单位),要么满足条件。
❑QTRY_VERIFY:类似于QTRY_VERIFY_WITH_TIMEOUT,但是超时默认设置为5秒。
❑QVERIFY2、QTRY_VERIFY2_WITH_TIMEOUT和QTRY_VERIFY2:这些宏与前面的宏非常相似,名称也惊人地相似,只不过在测试失败的情况下,函数还将输出给定的消息。
❑QCOMPARE:用来比较实际值和期望值。该宏除了还输出实际值和期望值供以后参考之外,与QVERIFY类似。
❑QTRY_COMPARE_WITH_TIMEOUT:类似于QCOMPARE,但是该函数尝试比较实际值和期望值,要么直到达到给定的超时(以毫秒为单位),要么它们相等。
❑QTRY_COMPARE:与QTRY_COMPARE_WITH_TIMEOUT类似,但是将超时设置为默认的5秒。

void HelloTestTest::testPixelCount_data()
{
    QTest::addColumn<QString>("filename");
    QTest::addColumn<int>("pixelcount");

    QTest::newRow("huge image") << "c:/dev/imagehd.jpg" << 2280000;
    QTest::newRow("small image") << "c:/dev/tiny.png" << 51200;
}

注意,数据函数名称的末尾有一个附加的“_data”。可将QTest中的测试数据看作一个表,这就是为什么在数据函数中使用addColumn函数来创建新的列(或字段),并使用addRow向其添加新的行(或记录)。前面的代码可得到与表10-1类似的测试数据表:

                                                      表10-1 测试数据表

现在,可以修改测试函数testPixelCount,以使用该测试数据而不是在同一个函数内提供的单个文件名。新的testPixelCount函数与下面类似(同时,用QCOMPARE替换QVERIFY,以得到更好的测试日志输出):

void HelloTestTest::testPixelCount()
{
    PixelCounter c;
    QFETCH(QString, filename);
    QFETCH(int, pixelcount);
    QCOMPARE(c.countPixels(filename), pixelcount);
}

请重点注意,对于数据函数内创建的测试数据中的每一列,必须为QFETCH提供它们的准确数据类型和元素名称。如果再次执行测试,测试框架将调用testPixelCount,调用次数与测试数据的行数相同,每次都通过获取和使用一个新行并记录输出来运行测试函数。使用数据驱动测试功能有助于保持实际测试函数的完整性,测试数据不是在测试函数中创建,而是从简单且结构化的数据函数中获取。可以将其扩展为从磁盘上的文件或其他输入方法(如网络位置)来获取测试数据。当数据函数存在时,无论数据来自何处,数据都应该能够完全呈现且能够正确地进行结构化。

10.2.3 基准测试
QTest提供了QBENCHMARK和QBENCHMARK_ONCE宏,来评估函数调用或任何其他代码片段的性能(基准)。这两个宏的唯一不同之处是,对一段代码的性能进行评估时重复这段代码的次数不同,显然第二个宏只运行一次代码。可以按下列方式使用这两个宏:
QBENCHMARK
{
    //Piece of code to be benchmarked
}
同样,可以在前面的示例中使用这两个宏来度量PixelCounter类的性能。可以将下列代码添加到testPixelCount函数的末尾处:
QBENCHMARK
{
    c.countPixels(filename);
}
如果再次运行测试,在测试日志输出中,会看到类似于下面内容的输出。注意,这些数字只是在随机测试PC上运行的示例,在不同的系统上可能会有显著的不同:
23 msecs per iteration (total:95,iterations:4)
上面的测试输出意味着每次用一个特定的测试图像来测试函数时用了23毫秒。此外,迭代次数为4,基准测试所用的总时间大约是95毫秒。

完整的项目代码如下:

HelloTest.pro

QT       += testlib

QT       -= gui

TARGET = tst_hellotesttest
CONFIG   += console
CONFIG   -= app_bundle

TEMPLATE = app

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
        tst_hellotesttest.cpp \ 
    pixelcounter.cpp

DEFINES += SRCDIR=\\\"$$PWD/\\\"

HEADERS += \
    pixelcounter.h

win32: {
    include("./opencv.pri")
}

unix: !macx{
    CONFIG += link_pkgconfig
    PKGCONFIG += opencv
}

unix: macx{
INCLUDEPATH += "/usr/local/include"
LIBS += -L"/usr/local/lib" \
    -lopencv_world
}
pixelcounter.h
#ifndef PIXELCOUNTER_H
#define PIXELCOUNTER_H

#include <QObject>
#include <QFile>

#include "opencv2/opencv.hpp"

class PixelCounter : public QObject
{
    Q_OBJECT
public:
    explicit PixelCounter(QObject *parent = nullptr);

    int countPixels(QString fname);

signals:

public slots:
};

#endif // PIXELCOUNTER_H

pixelcounter.cpp

#include "pixelcounter.h"
#include <QDebug>

PixelCounter::PixelCounter(QObject *parent) : QObject(parent)
{

}

int PixelCounter::countPixels(QString fname)
{
    cv::Mat image = cv::imread(fname.toStdString());
    //qDebug() << "PixelCounter::countPixels=======fname=====" << fname << "======image=====" << image.empty();
    if(image.empty())
    {
        return 0;
    }
    else
    {
        qDebug() << "PixelCounter::countPixels==================rows,cols=====" << image.rows << image.cols;
        return image.rows * image.cols;
    }
}

tst_hellotesttest.cpp

#include <QString>
#include <QtTest>
#include <QDebug>
#include "pixelcounter.h"

class HelloTestTest : public QObject
{
    Q_OBJECT

public:
    HelloTestTest();

private Q_SLOTS:
    void testPixelCount();
    void testPixelCount_data();
};

HelloTestTest::HelloTestTest()
{
    qDebug() << "HelloTestTest=================================";
}

void HelloTestTest::testPixelCount_data()
{
    QTest::addColumn<QString>("filename");
    QTest::addColumn<int>("pixelcount");
   
    QTest::newRow("image") << "E:/Image/aa.png" << 688*793;
}

void HelloTestTest::testPixelCount()
{
    PixelCounter c;
    QFETCH(QString, filename);
    QFETCH(int, pixelcount);
    qDebug() << "HelloTestTest=============filename====================" << filename << "  pixelcount=======" << pixelcount;
    QCOMPARE(c.countPixels(filename), pixelcount);

    QBENCHMARK
    {
        c.countPixels(filename);
    }
}


QTEST_APPLESS_MAIN(HelloTestTest)

#include "tst_hellotesttest.moc"

10.2.4 GUI测试
类似于执行特定任务的测试类,还可以创建用于测试GUI功能或控件行为的单元测试。在这种情况下,唯一的区别是:GUI需要鼠标单击、按键以及类似的用户交互。QTest支持用Qt创建的GUI测试,即通过模拟鼠标单击以及其他用户交互进行测试。可以用QTest命名空间中的以下函数来编写用于执行GUI测试的单元测试。请注意,几乎所有这些函数都依赖于这样一个事实:Qt中的所有控件和GUI组件都是QWidget的子类:
❑keyClick:用来模拟在键盘上按键。这个函数有许多重载版本。可以有选择地提供修改键(ALT、CTRL等)和/或按键之前的延迟时间。keyClick不应该和mouse-Click混淆,这个内容稍后会介绍,keyClick指的是单个按键和释放操作,这就产生了一次单击。
❑keyClicks:与keyClick十分相似,但是可用来模拟一个按键序列,同样可用可选的修改键或延迟。
❑keyPress:与keyClick非常相似,但是它只模拟按下按键,不模拟释放按键。如果需要模拟按住一个键,这是非常有用的。
❑keyRelease:与keyPress相反,只模拟按键的释放,不模拟按下按键。如果想要模拟释放一个之前使用keyPress按住的键,则使用keyRelease。
❑keyEvent:这是键盘模拟函数的更高级版本,有一个额外的动作参数,该参数定义了按键是否被按下、释放、单击(按下和释放),或是否是快捷键。
❑mouseClick:与keyClick类似,但它模拟鼠标单击。这就是为什么给这个函数提供的键是鼠标按键,比如左、右、中等等。键值应该是Qt::MouseButton枚举的一项。在模拟单击之前,它还支持键盘修改键和延迟时间。此外,该函数和所有其他鼠标模拟函数,还会接收一个可选的点(QPoint),该点包含控件(或窗口)中将要单击的位置。如果提供空点,或者忽略这个参数,那么模拟单击将发生在控件的中间位置。
❑mouseDClick:是mouseClick函数的双击版本。
❑mousePress:与mouseClick非常相似,但是只模拟鼠标按钮按下而不释放。如果想要模拟按住一个鼠标按钮时,mousePress是非常有用的。
❑mouseRelease:与mousePress相反,它只模拟的释放,而不模拟按下。可以用来模拟在一段时间后释放一个鼠标按钮。
❑mouseMove:可用来模拟鼠标光标在控件上移动。必须为该函数输入一个点和一个延迟。与其他鼠标交互函数类似,如果没有设置点,那么就会将鼠标移动到控件的中间点。当与mouseMove与mousePress以及mouseRelease共同使用时,该函数可以用来模拟和测试拖放。
让我们创建一个简单的GUI测试,以熟悉上面这些函数在实践中是如何使用的。假设想要测试一个已经创建的窗口或控件,必须首先在一个Qt单元测试项目中包含该窗口或控件。因此,从创建单元测试项目开始,类似于在前面的例子以及在第一个测试项目中所做的工作。在项目创建过程中,确保还选择了QtWidgets作为所需的模块之一。然后,将控件类文件(可能是头文件、源文件以及UI文件)添加到测试项目中。在我们的例子中,假设有一个简单的GUI,上面有一个按钮和标签。每次按下按钮,标签上的数字就会乘以2。为测试该功能或者其他的GUI功能,必须首先保证能够公开表单、容器控件或窗口上的控件,让测试类可以访问它们。在实现这一目标的众多方法中,最快速和最简单的方法是在类声明中作为公共成员定义相同的控件。然后,只需将ui变量(在使用“New File or Project”向导创建的所有Qt控件中都可以找到)中的类赋给类范围成员。假设分别将窗口上的按钮和标签命名为nextBtn和infoLabel(当使用设计器对它们进行设计时),则必须在类声明的公共成员中定义以下内容:
QPushButton *nextBtn;
QLabel *infoLabel;
必须在构造函数中对它们进行赋值,如下所示:
ui->setupUi(this);
this->nextBtn = ui->nextBtn;
this->infoLabel = ui.infoLabel;
确保总是在setupUi调用之后对使用设计器和UI文件创建的控件进行赋值。否则,应用程序肯定会崩溃,因为在调用setupUi之前并没有真正创建控件。现在,假设将我们的控件类命名为TestableForm,在测试类中有一个私有的testGui槽。记住,每次按下nextBtn时,infoLabel上的数字就会乘以2,因此在testGui函数中包含以下代码:

void GuiTestTest::testGui()
{
    TestableForm t;

    QTest::mouseClick(t.nextBtn, Qt::LeftButton);
    QCOMPARE(t.infoLabel->text(), QString::number(1));

    QTest::mouseClick(t.nextBtn, Qt::LeftButton);
    QCOMPARE(t.infoLabel->text(), QString::number(2));

    QTest::mouseClick(t.nextBtn, Qt::LeftButton);
    QCOMPARE(t.infoLabel->text(), QString::number(4));
    //repeated until necessary
}

非常重要的是,还必须替换下列代码行:
QTEST_APPLESS_MAIN(GuiTestTest)
增加下列代码行:
QTEST_MAIN(GuiTestTest)
否则,不会在后台创建QApplication,测试将会失败。用Qt测试框架测试GUI时,记住这一点很重要。现在,如果尝试运行单元测试,那么将单击nextBtn控件三次,每次都要检查由infoLabel显示的值是否正确。万一失败的话,在输出中将记录相关信息。这很简单,但问题是,如果需要交互的次数增加了怎么办?如果必须执行一长串的GUI交互,该怎么办呢?为了解决这个问题,可以使用数据驱动测试和GUI测试来轻松地重放GUI交互(或事件,这是在Qt框架中的叫法)。请记住,为了在测试类中让测试函数有数据函数,必须创建一个新函数,该函数的名称与测试函数相同但带有后缀“_data”。因此,可以创建一个名为“testGui_data”的新函数,它将准备一组交互和结果,并使用QFETCH将其传递给测试函数,就像前面示例中使用的那样:

void GuiTestTest::testGui_data()
{
    QTest::addColumn<QTestEventList>("events");
    QTest::addColumn<QString>("result");

    QTestEventList mouseEvents; // three times
    mouseEvents.addMouseClick(Qt::LeftButton);
    mouseEvents.addMouseClick(Qt::LeftButton);
    mouseEvents.addMouseClick(Qt::LeftButton);
    QTest::newRow("mouse") << mouseEvents << "4";

    QTestEventList keybEvents; // four times
    keybEvents.addKeyClick(Qt::Key_Space);
    keybEvents.addDelay(250);
    keybEvents.addKeyClick(Qt::Key_Space);
    keybEvents.addDelay(250);
    keybEvents.addKeyClick(Qt::Key_Space);
    keybEvents.addDelay(250);
    keybEvents.addKeyClick(Qt::Key_Space);
    QTest::newRow("keyboard") << keybEvents << "8";
}

QTestEventList类是Qt测试框架中的一个便捷的类,可以用来轻松地创建GUI交互列表并对它们进行模拟。它包含的函数可以用来添加之前介绍过的所有可能的交互,这些交互是可以使用Qt测试执行的可能事件的一部分。

要使用这个数据函数,需要重写testGui函数,如下所示:

void GuiTestTest::testGui()
{
    TestableForm t;

    QFETCH(QTestEventList, events);
    QFETCH(QString, result);

    events.simulate(t.nextBtn);

    QCOMPARE(t.infoLabel->text(), result);
}

与任意一个数据驱动测试类似,QFETCH获取由数据函数提供的数据。然而,在这种情况下,存储的数据是一个QEventList,而且它被填充了一系列必需的交互。从错误报告中重放一系列事件,以重现、修复并进一步测试一个特定问题时,这种测试方法是非常有效的。testcase projects

10.2.5 测试用例项目

在前文及其对应的示例中,我们看到了一些简单的测试用例,并使用Qt测试函数解决这些测试用例。我们学习了数据驱动和GUI测试,以及如何将这两者结合起来回放GUI事件并执行更复杂的GUI测试。我们在每一种情况下学习到的相同方法都可以进一步扩展,以应用于更复杂的测试用例。在本节中将学习如何确保在构建项目时自动执行测试。当然,这取决于测试所需的时间以及我们的偏好,我们可能希望暂时跳过自动测试,但是最终,我们需要在构建项目时轻松地执行测试。为了能够自动运行Qt项目的测试单元(让我们将其称为主项目),首先,需要确保一直使用subdirs模板来创建它们,然后将单元测试项目配置为测试用例项目。这也可以通过已经存在且不在subdirs模板中的项目来实现。只需按照本节介绍的步骤,将一个现有的项目添加到一个subdirs模板,并创建一个单元测试(配置为测试用例),以便在一旦建立主项目时就自动运行该单元测试:

1.首先,在Qt Creator的欢迎模式下使用“New Project”按钮创建一个新项目,或者从“File”菜单中选择“New File or Project”。

2.如图10-10所示,确保选择了“Subdirs Project”,单击“Choose”:

                                                    图10-10 新建项目界面截图

3.为项目选择一个名称,可以与主项目名称相同。假设将其称为“computer_vision”。继续前进,在最后一个对话框中,单击“Finish&Add Subproject”按钮。如果正在从头开始创建一个项目,那么只需按照本书介绍的方法创建自己的项目。否则,如果想添加现有的项目(假设在名为“src”的文件夹中),只要单击“Cancel”,然后将要为其构建测试的现有项目复制到新创建的subdirs项目文件夹中。然后,打开“computer_vision.pro”文件,将它修改成类似下面的代码行:

TEMPLATE = subdirs
SUBDIRS += srcs

4.现在可以创建一个单元测试项目,它也是“computer_vision subdirs”项目的子项目,并对其进行编程,以测试在src文件夹(主项目,也就是实际的应用程序本身)中存在的类。因此,在项目窗体中再次在“computer_vision”上单击右键,选择“New Subproject”,使用前几节中学到的所有知识,开始创建单元测试。

5.创建测试之后,应该能够在不考虑主项目的情况下单独运行它,并查看测试结果。但是,为了确保将它标记为测试用例项目,需要将下面的代码行添加到单元测试项目的*.pro文件:

CONFIG += testcase

6.最后,需要切换到Qt Creator中的项目模式,并在“Make arguments”字段中添加“check”,如图10-11所示。请先使用“Details”扩展按钮展开“Make”部分,否则它不可见。

                                    图10-11 Qt Creator中的项目模式界面截图

现在,不管是否明确运行单元测试项目,每次运行或尝试构建主项目时,测试都会自动执行。这是一个非常有用的技术,可以确保一个库的更改不会对另一个库产生负面影响。关于这项技术最需要注意的一点是,实际上测试结果会影响构建结果。也就是说,当构建项目时,你将注意到在自动测试中是否有失败,并且测试结果将会在Qt Creator的编译器输出窗体中可见,可以使用底部栏或按“ALT+4”键激活该窗体。

完整的项目代码如下:

GuiTest.pro

QT       += widgets testlib

TARGET = tst_guitesttest
CONFIG   += console
CONFIG   -= app_bundle

TEMPLATE = app

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0


SOURCES += \
        tst_guitesttest.cpp \ 
    testableform.cpp

DEFINES += SRCDIR=\\\"$$PWD/\\\"

FORMS += \
    testableform.ui

HEADERS += \
    testableform.h

testableform.h

#ifndef TESTABLEFORM_H
#define TESTABLEFORM_H

#include <QWidget>
#include <QPushButton>
#include <QLabel>

namespace Ui {
class TestableForm;
}

class TestableForm : public QWidget
{
    Q_OBJECT

public:
    explicit TestableForm(QWidget *parent = 0);
    ~TestableForm();

    QPushButton *nextBtn;
    QLabel *infoLabel;

private slots:
    void on_nextBtn_pressed();

private:
    Ui::TestableForm *ui;
};

#endif // TESTABLEFORM_H

testableform.cpp

#include "testableform.h"
#include "ui_testableform.h"

TestableForm::TestableForm(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::TestableForm)
{
    ui->setupUi(this);

    this->nextBtn = ui->nextBtn;
    this->infoLabel = ui->infoLabel;
}

TestableForm::~TestableForm()
{
    delete ui;
}

void TestableForm::on_nextBtn_pressed()
{
    if(ui->infoLabel->text().trimmed().isEmpty())
    {
        ui->infoLabel->setText("1");
    }
    else
    {
        int n = ui->infoLabel->text().toInt();
        ui->infoLabel->setText(QString::number(n*2));
    }
}

tst_guitesttest.cpp

#include <QString>
#include <QtTest>
#include <QTestEventList>

#include "testableform.h"

class GuiTestTest : public QObject
{
    Q_OBJECT

public:
    GuiTestTest();

private Q_SLOTS:
    void testGui();
    void testGui_data();
};

GuiTestTest::GuiTestTest()
{
}

void GuiTestTest::testGui_data()
{
    QTest::addColumn<QTestEventList>("events");
    QTest::addColumn<QString>("result");

    QTestEventList mouseEvents; // three times
    mouseEvents.addMouseClick(Qt::LeftButton);
    mouseEvents.addMouseClick(Qt::LeftButton);
    mouseEvents.addMouseClick(Qt::LeftButton);
    QTest::newRow("mouse") << mouseEvents << "4";

    QTestEventList keybEvents; // four times
    keybEvents.addKeyClick(Qt::Key_Space);
    keybEvents.addDelay(250);
    keybEvents.addKeyClick(Qt::Key_Space);
    keybEvents.addDelay(250);
    keybEvents.addKeyClick(Qt::Key_Space);
    keybEvents.addDelay(250);
    keybEvents.addKeyClick(Qt::Key_Space);
    QTest::newRow("keyboard") << keybEvents << "8";
}

void GuiTestTest::testGui()
{
    TestableForm t;

    QFETCH(QTestEventList, events);
    QFETCH(QString, result);

    events.simulate(t.nextBtn);

    QCOMPARE(t.infoLabel->text(), result);
}


QTEST_MAIN(GuiTestTest)

#include "tst_guitesttest.moc"

运行结果:

10.3 小结

在本章中,你学习了如何使用Qt Creator进行调试及其提供的功能,以便进一步分析代码、查找问题并尝试使用断点、调用堆栈查看器等对问题进行修复。这只是对使用调试器所完成的一小部分功能的体验,其目的是引导读者独立使用调试器,养成自己的编程和调试习惯,以便更轻松地克服编程问题。除了调试和开发人员级别的测试之外,还学习了Qt中的单元测试,随着使用Qt框架编写的应用程序和项目的数量不断增长,Qt单元测试变得尤为重要。测试自动化是当今应用程序开发行业的热门话题之一,对Qt测试框架有一个清晰的认识将帮助你开发更好、更可靠的测试。习惯为项目编写单元测试是很重要的,即使对于很小的项目也是如此。对于初学者或者业余爱好者来说,测试一个应用程序并避免返工的成本是不容易看到的,因此,为你的开发生涯的后期阶段肯定会遇到的问题做好准备是一个好办法。

在本书最后几章,我们也越来越多地关注使用Qt和OpenCV的应用程序开发的最后阶段。因此,下一章将学习如何为最终用户部署应用程序。还将学习应用程序的动态和静态链接,并创建可以轻松地安装在不同操作系统的计算机上的应用程序包。下一章将是我们在桌面平台上用OpenCV和Qt进行计算机视觉之旅的最后一章。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值