QT 开发基础知识(七)

原文:Foundations of Qt Development

协议:CC BY-NC-SA 4.0

十五、构建 Qt 项目

他的书依靠 QMake 通过使用标准的项目文件来构建示例应用程序,而没有使用任何高级特性。然而,QMake 也可以用于管理高级项目和处理产生多个可执行文件、库和插件的项目。本章介绍了当您想要创建更复杂的 Qt 项目时,您将需要的一些最常见的特性。

您还将了解到 Qmake 的一个很好的替代品:Kitware 的 CMake ( [www.cmake.org/](http://www.cmake.org/))。和 QMake 一样,CMake 也是一个开源的跨平台构建系统。值得讨论 CMake,因为它被作为 Qt 最著名的用户之一,KDE 项目([www.kde.org/](http://www.kde.org/))的构建工具。

QMake

QMake 是 Qt 附带的构建工具。它是通用的,可以用于在 Qt 支持的所有平台上构建大多数项目。它用于从项目文件生成构建环境。它还可以为 Visual Studio 和 Xcode 创建 Makefiles 和项目文件。

QMake 项目文件

要开始使用 QMake,让它通过执行以下命令为自己创建一个项目文件:

qmake -project
QMake 将在当前目录和子目录中查找它所识别的文件,然后将它们添加到一个标准化的项目中以构建一个应用程序。

**注意**你应该只在创建新项目的时候使用`-project`选项。向现有项目添加文件时,您需要手动将它们添加到项目文件中;否则,您将丢失对项目文件所做的任何更改。

清单 15-1 显示了一个由 QMake 生成的项目文件。如您所见,以`cpp`、`h`和`ui`结尾的文件已经被识别。QMake 可以识别大多数在基于 Qt 的软件项目中使用的文件结尾,但是这三个是这个项目中唯一可用的文件扩展名。
让我们从最上面开始,详细地看一下项目文件。首先要注意的是,注释以一个散列字符(`#`)开始,它将该行的其余部分标记为注释。第一个未注释的行(不算空行)读`TEMPLATE = app`;它将变量`TEMPLATE`设置为`app`。现在`TEMPLATE`有了一个特殊的含义,因为它的值被用来决定你试图构建的项目的类型— `app`意味着你正在构建一个应用程序。(本章稍后将介绍其他模板选项。)
在分别设置`TARGET`、`DEPENDPATH`和`INCLUDEPATH`的`TEMPLATE`线之后有三条线。将`TARGET`设置为 nothing 意味着生成的可执行文件将以项目文件命名。例如,如果项目文件被命名为`superapp.pro`,那么生成的可执行文件将被命名为`superapp`(或者在 Windows 上被命名为`superapp.exe`)。如果你给`TARGET`指定一个名字而不是什么都没有,那么这个名字将代替项目文件的名字。
另外两个变量`DEPENDPATH`和`INCLUDEPATH`被设置为。,所以 QMake 知道您将项目的文件保存在当前目录中。两者的区别在于,QMake 在映射项目中的依赖关系时使用`DEPENDPATH`,而`INCLUDEPATH`被传递给编译器,告诉它在哪里寻找包含的文件。可以为这些变量添加更多的路径——只需用空格将它们隔开。

**注意**目录。(点号)指当前目录,正如目录..(两点)指包含当前目录的目录。

在指定了模板、选择了生成的可执行文件的名称并通知 QMake 头文件保存在哪里之后,就该告诉它编译什么了。这样做需要三个变量:`SOURCES`、`HEADERS`和`FORMS`。
`SOURCES`用于保存以`cpp`、`cc`或`cxx`结尾的源文件,具体取决于您的个人喜好。`HEADERS`用于头文件:`h`、`hpp`或`hxx`。最后,`FORMS`用于设计器表单:`ui`。

**清单 15-1** *一个自动生成的项目文件*
######################################################################

# Automatically generated by qmake (2.01a)19\. mar 18:20:02 2007

######################################################################

TEMPLATE = app

TARGET =

DEPENDPATH += .

INCLUDEPATH += .

# Input

HEADERS += mainwindow.h otherdialog.h preferencedialog.h

FORMS += otherdialog.ui preferencedialog.ui

SOURCES += main.cpp mainwindow.cpp otherdialog.cpp preferencedialog.cpp 
在项目文件中,使用了两个不同的赋值操作符:`=`和`+=`。第一个,`=`,替换现有值;后者,`+=`,增加了更多的现有价值。为了理解结果,你需要知道 QMake 是什么变量。
QMake 变量是字符串列表,可以放在一行中,用空格分开,或者分成不同的赋值。下面一行:
SOURCES += main.cpp dialog.cpp mainwindow.cpp
相当于这样:
SOURCES += main.cpp

SOURCES += dialog.cpp    \

           mainwindow.cpp
请注意,该赋值使用了`\`字符分布在两行上。通过用反斜杠结束一行,换行符被视为空白,并且该行被视为继续。
如果你反复使用`+=`,然后不小心使用了`=`,你很可能会碰到一些看起来很奇怪的 bug。因为`=`操作符替换了变量的内容,所有先前的值都将丢失。奇怪行为的另一个来源可能是当重复使用`+=`并且两次意外添加相同的值时。为了避免这种情况,您可以使用`*=`操作符,该操作符向变量添加一个值,但前提是该值不存在。
还有另一个操作符可以用来控制 QMake 变量的内容:`-=`。这个操作符从列表中删除值,当您想要从 Qt 中删除一个默认选项时可以使用它。例如,下面一行从生成项目中移除用户界面模块:
QT -= gui
您必须移除该模块,因为默认情况下它是`QT`变量的一部分。
**更多项目文件选项**
清单 15-1 中自动生成的项目文件中使用的变量不是唯一可用的变量。实际上,QMake 使用了 100 多个变量——太多了,本文无法一一介绍。这里列出了最有用的,而不是全部:

  • DEFINES:该变量包含将用于配置项目的预处理器定义。有许多定义可以用来微调最终的 Qt 应用程序。例如,QT_NO_DEBUG_OUTPUT用于关闭qDebug消息,QT_DEBUG_PLUGINS用于打开关于插件加载的调试信息。这些定义被传递给编译器,因此您可以在代码中使用它们。
  • LIBS:使用该变量链接库。使用-L path命令将路径添加到目录列表中,以搜索库。然后使用-l library(破折号,小写 L,库名)添加对实际库的引用。为了链接库/home/e8johan/mylib/libthelibrary.a,项目文件行应该是LIBS += -L/home/e8johan/mylib –lthelibrary。QMake 负责将这些标志(-L-l)转换成当前使用的编译器。
  • DESTDIR:如果需要控制结果文件的最终位置,可以使用这个变量。例如,通过将它设置为../bin,结果文件将被放置在与包含项目文件的目录相同的目录级别上的bin目录中。

当您构建一个 Qt 应用程序时,您最终会得到许多中间文件。设计器用户界面由用户界面编译器编译成头文件,头文件由元对象编译器编译成 C++ 源文件,所有 C++ 源文件都编译成目标文件。将这些文件与源文件和头文件放在同一个目录中会导致混乱的局面。当然,运行make clean将会清除它,但是您可以使用以下变量做得更好:

  • OBJECTS_DIR:控制中间目标文件的放置位置。
  • UI_DIR:控制用户界面编译器生成的中间文件的位置。
  • MOC_DIR:控制元对象编译器生成的中间文件放在哪里。

一个好的策略是将目标文件放在./obj中,将uic文件放在./ui中,将moc文件放在./moc目录中,方法是在项目文件中添加以下几行:

OBJECTS_DIR = obj UI_DIR = ui MOC_DIR = moc


注意添加完这些行之后,QMake 将尝试自动创建目录。在 Unix 平台上,通常使用目录.obj.ui.moc,因为它们在默认情况下是隐藏的。


使用 QMake 管理资源

当将资源嵌入到可执行文件中时,创建一个从项目文件中引用的资源文件。资源可以是图标、翻译或应用程序使用的任何其他文件。(参考第四章了解更多关于资源文件格式的信息。)


注意这里说的资源是 Qt 资源,不是 Windows 资源。


资源文件通常具有文件扩展名qrc。它们被添加到项目文件的RESOURCES变量中,这使得资源编译器rcc将指定的资源编译成一个中间 C++ 源文件。您可以通过使用RCC_DIR变量来控制这些中间文件的位置。

配置 Qt

在构建过程中,有几种方法可以配置 Qt。例如,您可以控制包含 Qt 的哪些部分以及这些部分的行为方式,这使您能够构建只使用 Qt 中所需部分的应用程序,从而减少可执行文件和内存占用。在对DEFINES变量的讨论中,您已经看到了一些可以用来做这件事的定义,但是您将在本节中看到更多。

控制包含 Qt 的哪些部分的两个主要变量是QTCONFIGQT控制项目中包含的模块。默认包括coregui。以下模块可用(取决于您使用的 Qt 版本):

  • core:核心模块
  • gui:用户界面模块QtGui,用于所有具有图形用户界面的应用程序
  • network:模块QtNetwork,用于第十四章
  • opengl:模块QtOpenGL,用于第七章
  • sql:模块QtSql,用于第十三章
  • svg:模块QtSvg,用于第七章
  • xml:模块QtXml,用于第八章
  • qt3support:Qt3Support模块,用于使 Qt 3 应用程序移植到 Qt 4 更加容易

第二个主要变量是CONFIG变量,默认情况下通常以合理的方式设置。最常用的值如下:

  • thread:如果包含的话,应用程序是支持多线程的。
  • 如果包含的话,Windows 应用程序将有一个控制台。例如,该控制台用于显示qDebug消息。
  • release:以发布模式构建项目。
  • debug:在调试模式下构建项目。
  • debug_and_release:在发布和调试模式下构建项目。
  • plugin:构建一个插件。
  • dll:构建一个动态链接库,也称为共享对象。
  • qttestlib:为构建单元测试添加 Qt 支持库。

构建一个 QMake 项目

为 Qt 项目创建项目文件后,需要运行 QMake 来创建适当的 Makefile 或项目。最简单的方法是在与项目文件相同的目录下,在命令行界面中键入qmake。它将使用平台默认值来生成一个合适的 Makefile。

你也可以使用 QMake 为 Visual Studio 生成一个项目文件。只需运行qmake -t vcapp来生成这样一个文件(用vclib替换vcapp来构建一个库项目)。要为 Xcode 生成一个项目文件,运行qmake -spec macx-xcode

您还可以在 QMake 调用中添加项目文件行。例如,qmake "CONFIG+=console"相当于将行CONFIG+=console添加到您的项目文件中。

如果您选择使用 QMake 创建 Makefile,那么您可以使用一个简单的make命令来构建您的项目(如果您使用的是 Visual Studio,那么可以使用nmake)。你可以使用make clean清理你的中间文件。稍微残酷一点的步骤是运行make distclean,它清理所有生成的文件,包括 Makefile。您必须再次运行 QMake 来获得make的 Makefile。

使用不同的平台

在使用平台无关的工具包(比如 Qt)时,您可能希望能够处理平台细节,这有很多原因。例如,您可能希望在不同的平台上使用不同的图标,或者拥有一段依赖于平台的自定义源代码。根据所使用的平台,QMake 使得以稍微不同的方式构建项目变得容易。

不同的平台使用一个叫做作用域的概念来处理。Qt 支持很多作用域,但最常见的是:

  • debug:项目正在调试模式下构建。
  • release:项目正在以发布模式构建。
  • 这个项目是在 Windows 环境下构建的。
  • 这个项目是在 Mac OS X 环境下构建的。
  • unix(包括 Linux):项目正在 Unix 环境中构建。

您可以用两种不同的方式处理作用域。你可以使用括号,如图所示,在这里选择if-else结构库:

win32 {   LIBS += -lmywin32lib } else macx {   LIBS += -lmymacxlib } else {   LIBS += -lmyunixlib }

您可以使用:运算符组合作用域;比如macx:debug: ..。相当于写macx { debug {} }。运算符带来了指定范围的另一种方法。您可以这样设置LIBS变量:

win32:LIBS += -lmywin32lib macx:LIBS += -lmymacxlib !win32:!macx:LIBS += -lmyunixlib

注意到!操作符被用来反转作用域。!win32:!macx的意思不是win32也不是macx

Windows 特有的功能

如果您希望能够显示调试输出,您可以将值console添加到CONFIG变量中。更微妙的方法是限制对 Windows 和调试模式应用程序的更改:

win32:debug:CONFIG += console

它确保您不会为以发布模式构建的应用程序打开控制台窗口。

为 Windows 平台构建应用程序时需要注意的另一个问题是应用程序图标(Explorer 在显示可执行文件时使用的图标)。


提示你使用setWindowIcon方法来设置应用程序窗口的图标。


Windows 上的应用程序图标由一个 Windows 资源表示(不要与 Qt 资源混淆),所以您必须创建一个 Windows 资源文件,并将其添加到 Qt 项目文件中。首先你需要创建一个文件格式为ico的图标。有许多工具可以创建这些文件(例如 Gimp 和 Visual Studio 中的图标编辑器,但是搜索互联网会发现许多替代工具)。

创建图标后,您需要创建 Windows 资源文件,这是一个文件扩展名为rc的文件。该文件应包含以下行。

IDI_ICON1 ICON DISCARDABLE "filename.ico"

用你的图标替换filename.ico。要将资源文件添加到项目文件中,只需添加一行代码RC_FILE += filename.rc,其中filename.rc是您的 Windows 资源文件。没有必要给这一行加上前缀win32 scope,因为在它不适用的平台上它会被忽略。

OS X 特有的特征

Mac OS X 和 Qt 支持的其他平台之间的最大区别是能够在几个处理器平台上运行相同的应用程序。可用的处理器 PowerPC 和 Intel x86 有许多不同之处——最麻烦的是字节序。确保总是使用 Qt 流来加载和存储数据——不仅是文件,还有数据库、网络流和其他可以被两个处理器读写的缓冲区。该问题存在于多字节值的字节顺序中。例如,如果您不决定坚持哪种字节序,一个平台上的 32 位整数读数0x12345678在另一个平台上会被读作0x78563412

在 Mac OS X 平台上配置 Qt 时,可以使用-universal标志,这使得创建通用二进制文件成为可能。您可以使用CONFIG变量以及ppcx86值来控制支持哪些处理器。键入CONFIG += x86 ppc创建一个通用项目,可以在任一平台上执行。

与 Windows 应用程序一样,OS X 应用程序也有应用程序图标。Mac 平台上使用的文件格式是icns。您可以使用几个工具创建icns文件(在互联网上搜索例子)。Apple 提供了图标编辑器,这是推荐使用的工具。在您创建了一个icns文件之后,您需要使用一行代码ICON = filename.icns将它添加到您的项目文件中,其中filename.icns是您的图标文件。

特定于 Unix 和 X11 的特性

在 Unix 系统上构建和部署通常比在 Windows 上更困难,因为 Unix 有很多种。对于每一种风格,都有几种不同的桌面环境。(桌面环境是用户看到和使用的,可以处理开始菜单、停靠、窗口样式等等。)处理所有这些组合意味着做事有几种方式,做正确的事情有许多变体。

另一个需要解决的问题是,Qt 库可能已经安装在您的目标系统上。您需要找出什么版本和在哪里,这至少可以通过两种方式来实现。一种方法是将您的应用程序静态链接到 Qt,这意味着如果 Trolltech 决定发布您的 Qt 版本的更新,将会有更大的可执行文件并且没有自动更新。

另一个选项仅适用于 Linux 系统。您可以要求系统支持 Linux 标准库(LSB ),因为 Qt 4.1 作为可选的 LSB 模块提供。请访问[www.linuxstandardbase.org](http://www.linuxstandardbase.org)了解更多信息。

现在简单看一下,在正确安装 Qt 应用程序后,如何将它集成到当前的桌面环境中。


提示欲了解更多信息,请访问[www.freedesktop.org](http://www.freedesktop.org)


让我们看看应用程序图标是如何设置的。Unix 二进制不知道图标的概念资源。相反,桌面条目文件用于描述每个应用程序。这些文件的文件扩展名为desktop,通常存储在$XDG_DATA_DIRS/applications/usr/share/applications中。清单 15-2 中的显示了一个示例文件。

**清单 15-2。**my application 项目的示例桌面文件

[Desktop Entry] Type=Application Name=My Application Exec=myapplication %F MimeType=image/x-mydata; Icon=/install/path/myicon.png

在清单中,[Desktop Entry]这一行告诉您接下来是一个桌面条目。接下来是Type,它告诉你条目将描述一个应用。根据Name的说法,这个应用程序叫做My ApplicationExec行告诉桌面发出什么命令来启动应用程序;这种情况下就是myapplication。如果用户试图打开一个或多个数据文件,那么%F部分告诉桌面在哪里列出文件名。使用定义 mime 类型的MimeType条目来处理这些数据文件和应用程序之间的连接;即应用程序处理的文件类型。

最后一行Icon,告诉你使用哪个图标。最简单的方法是指定图标的绝对路径。如果仅指定文件名,则必须确定存储图标文件的位置,以便桌面环境可以找到它。

在 Unix 上安装应用程序时,通常支持 make target install,这使用户能够键入make install将应用程序文件复制到一个全局位置。QMake 使用安装集支持这一点。

一个安装集是一个带有三个子值的 QMake 变量:pathfilesextra。我们来看一个例子。假设您想要安装一组插件,它们位于与项目文件相关的子目录plugins中。当应用程序安装完成后,您希望这些文件位于/usr/local/myapplication/plugins中。如下指定,其中最后一行将插件安装集添加到 install make 目标:

plugins.files = plugins/* plugins.path = /usr/local/myapplication/plugins INSTALLS += plugins

您还希望在一个名为plugins.lst的文件中有一个插件列表,这就是extra子值的用途。它使您能够指定在复制文件之前要运行的命令列表。通过添加下面一行,在插件被复制到适当位置之前创建了该列表:

plugins.extra = rm -f ./plugins/plugins.lst; ls −1 ./plugins > ./plugins/plugins.lst

这一行由一个rm命令组成,该命令删除任何现有的plugins.lst文件,因为如果存在的话,该列表将包含在插件列表中。然后执行一个ls命令,构建一个新的列表,通过管道传输到plugins.lst文件中。

有一个特殊的安装集,代表 QMake figures 想要复制的文件:target。通过指定一个路径并将其添加到INSTALLS,QMake 负责剩下的工作:

target.path = /usr/local/myapplication INSTALLS += target

因为可以在所有平台上使用 make 作为构建系统,所以建议使用平台范围来保护安装集(特别是,extra值中列出的命令需要适应不同的平台)。

使用 QMake 构建库

到目前为止,您一直在处理构建应用程序的项目。QMake 也可以用于构建库,包括静态库、动态库和插件(一种特殊的动态库)。要让 QMake 做到这一点,您必须将TEMPLATE变量改为lib

一个图书馆项目的例子显示在清单 15-3 中。该项目使用SOURCESHEADERS变量的方式与构建应用程序时相同。合并TARGETVERSION来创建结果库的文件名,这是避免版本问题的常用方法。因为不同版本的库有不同的名称,所以问题得以避免。


注意使用VERSION意味着你的库的名字会被修改。不要让这个迷惑你。


CONFIG变量用于控制正在构建的库的类型。通过添加值dll来构建动态库。其他可能的值有staticlib,它构建一个静态库,还有plugin,它用于构建插件。注意,添加值plugin也隐含地添加了值dll,因为插件是一个动态库。

清单 15-3。 一个建库的项目文件

`TEMPLATE = lib
TARGET = mylib
VERSION = 1.0.0
CONFIG += dll

HEADERS += mylib.h
SOURCES += mylib.cpp`

用于库的文件扩展名在不同的平台和编译器之间是不同的(这都是由 QMake 处理的)。例如,永远不要给TARGET变量指定文件扩展名;让 QMake 来处理它。

使用 QMake 构建复杂的项目

通常,构建一个库或一个应用程序就足够了,但有时您的项目由几个部分组成,从而产生几个库和几个应用程序。QMake 也足够强大来处理这些情况。让我们看看它会是什么样子。

这里显示的项目由一个库和一个应用程序组成。库叫base,应用叫app。项目文件的结构如清单 15-4 所示。主项目文件complex.pro位于基础层,还有目录binlibappincludesrcbinlib目录为空。

app目录包含应用程序的源代码和项目文件。include目录包含库的头文件;也就是说,库和应用程序之间共享的头文件。src目录包含库的源代码和项目文件。

两个空目录libbin,分别用于从src的内容构建的库和从app生成的应用程序二进制文件。


注意因为libbin目录仅用于保存构建的文件,所以您可以省略它们;QMake 会在被要求放置文件时创建它们。


清单 15-4。 复杂项目的文件和目录

|   complex.pro | +---bin +---lib | +---app |   |   app.pro |   |   appwindow.cpp |   |   appwindow.h |   |   main.cpp | +---include |       base.h | \---src     |   base.cpp     |   src.pro

主项目文件complex.pro如清单 15-5 所示。它用的是TEMPLATE,对你来说是新的。subdirs模板用于处理放置在不同子目录下的多个项目文件。需要注意的目录在SUBDIRS变量中列出。CONFIGordered告诉 QMake 按照它们被添加到SUBDIRS变量的顺序构建不同目录的项目。如果未指定,则构建顺序未定义。

**清单 15-5。**complex . pro 项目文件

TEMPLATE = subdirs SUBDIRS = src app CONFIG += ordered

整个文件告诉 QMake 首先在src目录中构建项目,然后在app目录中构建项目。让我们继续跟随 QMake 到src目录。

src目录中,QMake 找到了src.pro项目文件(参见清单 15-6 )。根据项目文件所在的目录来命名项目文件是一种常见的策略。如果你在一个目录中运行qmake -project,就会发生这种情况,但是你也可以手动创建项目文件。

src目录中文件的目的是构建一个应用程序使用的库;也就是app目录的内容。库的源代码保存在src,头文件保存在include,结果库放在lib。头文件保存在include中,因为它们在复杂项目的所有部分之间共享,而include目录包含了按照惯例所有部分共有的头文件。

项目文件的第一部分告诉 QMake 使用TEMPLATE变量创建一个库。然后使用TARGET指定库的名称,使用VERSION指定版本,并设置CONFIG以便创建一个静态库。

这个库打算在lib目录中结束,所以DESTDIR变量被设置为../lib,这是到那个目录的相对路径。

项目的头文件存储在项目全局包含目录中。您必须将该路径添加到INCLUDEPATHDEPENDPATH变量中。项目的源文件和项目文件存储在同一个目录中,所以DEPENDPATH也包含了对.目录的引用。

设置好包含文件和项目文件的路径后,列出SOURCESHEADERS。因为包含头文件的目录包含在DEPENDPATH变量中,所以不必给它添加相对路径;QMake 无论如何都会找到的。

清单 15-6。 用于构建库的 src.pro 项目文件

`TARGET = base
VERSION = 0.1.0
CONFIG += static

DESTDIR = …/lib

INCLUDEPATH += …/include
DEPENDPATH += . …/include

SOURCES += base.cpp
HEADERS += base.h`

在 QMake 访问了src目录后,它将继续访问app目录和app.pro项目文件。这个项目的目的是创建一个使用从src项目构建的库的应用程序。

app.pro项目文件如清单 15-7 所示。正如所料,它首先将TEMPLATE设置为app,表明您正在构建一个应用程序。然后,通过将TARGET设置为app并将DESTDIR设置为../bin,文件继续。这告诉 QMake 创建一个名为app(在 Windows 上为app.exe)的应用程序二进制文件,并将其放在bin目录中。

下一组线设置了INCLUDEPATHDEPENDPATH。包含路径被设置为同时包含.../include,因为应用程序使用位于.目录中的应用程序本地头文件和位于include目录中的复杂项目部分全局头文件。注意,全局头文件属于库项目,所以它们不包含在DEPENDPATH中。

接下来是LIBS行,这是由src.pro项目文件创建的库链接到这个项目的地方。第一个值-L../lib告诉 QMake 库存储在lib目录中。下一个值-lbase,告诉 QMake 将应用程序链接到base库。

项目文件的最后是源文件和头文件的列表。这些是应用程序项目的本地源文件。

清单 15-7。app.pro项目立项申请

`TEMPLATE = app
TARGET = app
DESTDIR = …/bin
INCLUDEPATH += . …/include
DEPENDPATH += .

LIBS += -L…/lib -lbase

SOURCES += appwindow.cpp main.cpp
HEADERS += appwindow.h`

要构建这个项目,使用命令行 shell 转到包含complex.pro的目录。从这里运行qmake会创建一个顶级的Makefile。奔跑中的make现在将依次造访srcapp。当访问每个子目录时,从本地项目文件创建一个Makefile,然后运行make来构建每个子项目。

结果是先建库;然后是申请。结果文件将被放在预期的位置:在binlib目录中。

CMake 构建系统

CMake 构建系统([www.cmake.org](http://www.cmake.org))是一个通用的构建系统。它并不专注于构建 Qt 应用程序;它专注于构建任何类型的应用程序。Qt 开发人员对此很感兴趣,因为 KDE 项目选择在 KDE 4 平台上使用 CMake。通用构建系统的缺点是使用 CMake 可能比使用 QMake 涉及的工作量稍多。然而,这并不意味着很难使用 CMake。该工具对 Qt 和 KDE 都有很好的支持。

虽然 CMake 和 QMake 都可以执行任何任务,但是 QMake 稍微偏向于 Qt 应用程序(尽管它在其他项目中也很有用)。另一方面,CMake 有一个 QMake 没有的特性:在源代码构建之外执行的能力,因此构建过程——及其所有中间文件——可以保存在源代码树之外。当您使用 CVS 或 Subversion 这样的版本控制系统时,这个特性非常方便。因为构建过程不把它的中间文件放在项目的源代码树中,所以它可以与所有不受版本控制的文件保持清洁。这大大降低了意外向源存储库添加中间文件的风险。


注意本文假设您使用的是最新版本的 CMake(至少是版本 2.4)。


使用 QMake 管理简单的应用程序

让我们从使用清单 15-1 中的 QMake 项目文件构建的同一个项目开始。它包括对源文件、头文件和用户界面文件的引用,以及控制 QMake 将产生什么和如何产生的配置(见清单 15-8 )。

所有的 CMake 项目都在一个名为CMakeLists.txt的文件中描述,这个文件对应于 QMake 使用的项目文件。每个 CMake 文件都是基于一个项目的,所以文件从使用PROJECT命令将项目名设置为basics开始。

您可以通过SET命令设置变量basics_SOURCESbasics_HEADERSbasics_FORMS继续。这些变量像 QMake 变量一样工作;它们被设置为一个值列表。SET命令接受一个参数列表,其中第一个参数是要设置的变量的名称。以下参数是值。

变量名都以前缀basics_开头。(这个约定不是必须的,但是很方便。)同样的约定告诉您为源、头和表单创建变量。这对于任何使用过 QMake 的人来说都很熟悉——这就是我们的目的。

接下来的两行介绍了 CMake 对 Qt 4 的支持。首先,FIND_PACKAGE用于定位Qt4包。这个包被标记为REQUIRED,这意味着如果 Qt 4 不存在,构建将会停止。然后使用INCLUDE命令设置包含 Qt 头文件和库的目录。在INCLUDE命令中,使用了${ variable }语法(指变量值)。

下一步是使用刚刚包含的命令。首先,让元对象编译器使用QT4_WRAP_CPP命令从头文件创建 C++ 源文件。第一个参数是一个变量名,它包含元对象编译器创建的 C++ 源文件的名称。

当元对象编译完成后,就可以用QT4_WRAP_UI命令将用户界面编译成头文件了。这个命令就像QT4_WRAP_CPP命令一样工作,产生一个变量,该变量包含对生成的文件的引用。

当使用 CMake 构建软件时,了解如何处理外部源代码是很重要的。源文件位于由CMAKE_CURRENT_SOURCE_DIR定位的源目录中,而中间文件和整个构建系统位于保存在CMAKE_CURRENT_BINARY_DIR中的二进制目录中。在源代码树内部构建时,这两个变量指向同一个目录;否则不会。

因为用户界面编译器生成的头文件是在编译时创建的,所以它们将位于二进制目录中。因为这些文件包含在位于源树中的源文件中,所以您必须在二进制目录和源树中查找包含文件。因此,使用INCLUDE_DIRECTORIES命令将CMAKE_CURRENT_BINARY_DIR添加到包含路径中。

在准备构建之前,您需要设置正确的预处理器定义来控制 Qt 库是如何构建的。Qt 定义保存在QT_DEFINITIONS变量中,该变量使用ADD_DEFINITIONS命令添加到构建环境中。

下一个命令ADD_EXECUTABLE,是在应用程序中生成结果的命令。它定义了一个名为basics的应用程序,这个应用程序是从源代码、元对象和用户界面头构建的。用户界面头不会被编译成任何东西,因为它们是头文件。但是,应用程序有必要引用它们,否则 CMake 会错过依赖它们的内容。如果可执行文件或库不直接或间接地依赖于构建系统的一部分,那么它就不会被构建。

在创建整个构建环境之前,您必须告诉 CMake 在项目文件的最后使用TARGET_LINK_LIBRARIES命令将应用程序链接到 Qt 库。先前在INCLUDE命令中导入了QT_LIBRARIES变量,它包含了该项目所需的所有库的引用。

清单 15-8。 一个基本 Qt 应用程序的 CMake 项目文件

`PROJECT( basics )

SET( basics_SOURCES main.cpp mainwindow.cpp otherdialog.cpp preferencedialog.cpp )
SET( basics_HEADERS mainwindow.h otherdialog.h preferencedialog.h )
SET( basics_FORMS otherdialog.ui preferencedialog.ui )

FIND_PACKAGE( Qt4 REQUIRED )
INCLUDE( ${QT_USE_FILE} )

QT4_WRAP_CPP( basics_HEADERS_MOC ${basics_HEADERS} )
QT4_WRAP_UI( basics_FORMS_HEADERS ${basics_FORMS} )

INCLUDE_DIRECTORIES( ${CMAKE_CURRENT_BINARY_DIR} )

ADD_DEFINITIONS( ${QT_DEFINITIONS} )

ADD_EXECUTABLE( basics b a s i c s S O U R C E S    {basics_SOURCES}    basicsSOURCES  {basics_HEADERS_MOC} ${basics_FORMS_HEADERS} )
TARGET_LINK_LIBRARIES( basics ${QT_LIBRARIES} )`

运行 CMake

要构建清单 15-8 所示的项目文件,你需要理解 CMake 是如何执行的。在查看命令行选项之前,先看看 CMake 提供的特性。

您可以从源代码树运行 CMake,就像 QMake 一样,将中间文件和结果留在源代码树中。还可以从远程目录运行 CMake,从而得到一个干净的源代码树。这意味着中间文件和结果文件(如应用程序和库)不会出现在源代码树中。通过保持源代码树没有这些文件,您可以将整个源代码树始终置于版本控制之下。这意味着在将源代码添加到版本控制中时,您不必清除任何不需要的文件,并且可以避免将中间文件添加到您可能会意外使用的版本控制系统中的风险。

您还可以使用 CMake 为许多不同的构建系统创建项目。不同的系统使用不同的生成器。在 Linux 和 Mac OS X 上,默认的生成器通常是有效的。这个生成器针对 GCC 系统。在 Windows 上,可能需要指定要使用的生成器。如果你和 MinGW 一起使用 Qt 的开源版本,要使用的生成器是MinGW Makefiles。您可以使用-G命令行选项来实现这一点——稍后将详细介绍。其他受支持的构建系统包括各种 Microsoft 编译器、Borland、Watcom、MSYS 和通用 Unix Makefiles。

当运行 CMake 时,在您的PATH环境变量中准备好您计划使用的所有工具是很重要的,包括您的编译器和 Qt 工具(比如uicmoc)。这些工具通常在路径中;如果没有,CMake 会告诉你它找不到什么。

那么,你如何实际运行 CMake 呢?第一步是启动一个命令提示符,将您定向到您的项目目录(包含CMakeLists.txt文件的目录)。在这个目录中,您可以使用下面的代码行在源代码树中构建项目:

cmake .

在 Windows 上,您可能需要使用-G命令行选项告诉 CMake 使用 MinGW 进行构建。这将为您提供以下命令:

cmake . -G "MinGW Makefiles"

传递给 CMake 的.指的是当前目录。它告诉 CMake 这是源目录。如果您想在源代码之外构建,这就是您告诉 CMake 要构建什么的方式。让我们像以前一样从项目目录开始,但是现在在您创建的单独目录中构建:

mkdir build cd build cmake ..

有时,您可能必须将-G "MinGW Makefiles"添加到cmake命令中,才能让它正常工作。通过在源代码树之外进行构建,您可以看到 CMake 创建了哪些文件以及系统是如何工作的。

可能给您带来麻烦的一个中心文件是 CMake 生成的CMakeCache.txt。如果您想要更改生成器,您需要删除这个文件以让 CMake 重新生成构建系统。

CMake 还创建了一个CMakeFiles目录,其中包含许多在构建过程中创建的中间文件。但是,元对象编译器和用户界面编译器生成的文件不放在这里。相反,它们被放在生成它们的文件旁边,或者,如果在源代码之外构建,则放在构建目录中的相应位置。

使用 CMake 管理资源

资源和 Qt 资源编译器的处理方式与元对象编译器和用户界面编译器相同。这些步骤包括设置一个变量,通常命名为project _RESOURCES,它包含项目的资源文件的名称。这个变量对应于 QMake 项目中的RESOURCES变量。

这个变量然后被传递给宏QT4_ADD_RESOURCES,它作为宏QT4_WRAP_CPPQT4_WRAP_UI工作。这意味着最左边的参数是一个变量,用来保存其余参数的结果。结果变量一般命名为project``_RESOURCES_SOURCES;然后将其添加到ADD_EXECUTABLE命令的可执行文件中。

下面的列表显示了取自一个虚构项目的相关行:

`SET( foo_RESOURCES foo.qrc )
QT4_ADD_RESOURCES( foo_RESOURCES_SOURCES ${foo_RESOURCES} )

ADD_EXECUTABLE( foo … ${foo_RESOURCES_SOURCES } … )`

配置 Qt 模块

因为 Qt 由许多模块组成,所以能够控制使用哪些 Qt 模块是很重要的。你可以通过使用一系列的QT_USE_QT moduleQT_DONT_USE_QT module变量来实现。在调用FIND_PACKAGE定位Qt4包之前,将这些变量设置为TRUE(使用SET命令)。这使得链接中使用的QT_LIBRARIES变量包含了对所需模块的引用。

包括和排除模块的一些可用变量如下所示:

  • QT_DONT_USE_QTCORE:不要链接到QtCore模块。这个变量几乎从来不用。
  • QT_DONT_USE_QTGUI:不要链接到QtGui模块。
  • QT_USE_QT3SUPPORT:链接到Qt3Support模块——用于帮助将 Qt 3 应用移植到 Qt 4。
  • QT_USE_QTASSISTANT:联动过程中包含助手模块。
  • QT_USE_QTDESIGNER:在联动过程中包含设计器模块。
  • QT_USE_QTNETWORK:联动过程中包含QtNetwork模块。
  • QT_USE_QTOPENGL:联动过程中包含QtOpenGL模块。
  • QT_USE_QTSQL:联动过程中包含QtSql模块。
  • QT_USE_QTXML:联动过程中包含QtXml模块。

注意使用Qt3Support模块时,间接链接到QtNetworkQtSqlQtXml模块。在某些平台上,有必要明确指定您正在使用这些模块。


使用不同的平台

使用 CMake 时,您会遇到与使用 QMake 时相同的特定于平台的问题。为了区分平台,有许多变量被设置为true,这取决于当前的 make 环境。最常见的列举如下:

  • WIN32 : true如果建筑在窗户上
  • APPLE : true如果建在 OS X
  • UNIX : true如果构建在类似 Unix 的环境中,包括 OS X 和 Linux
  • MINGW : true如果使用 MinGW 编译器构建
  • MSYS : true如果在 MSYS 环境中构建
  • MSVC : true如果使用微软编译器构建

要测试变量,使用IF( var )ELSE( var )ENDIF( var )建造。如果在 Windows 上使用 MinGW 作为构建环境,你可以使用清单 15-9 中的语句来区分平台:Windows、OS X 和 Unix/X11。只需用每个系统的平台细节替换注释行。


注意 CMake 认为一个#字符右边的所有文本都是注释。


清单 15-9。 区分可用平台

IF( MINGW )   # Windows, MinGW specifics here (i.e. Qt open source on Windows) ELSE( MINGW )   IF( APPLE )     # OS X specifics here   ELSE( APPLE )     # Linux / Unix specifics here   ENDIF( APPLE ) ENDIF( MINGW )

QMake 和 CMake 在平台细节方面的差异只影响您解决给定问题的方式。要解决的问题还是一样的。

当使用这里介绍的解决方案时,您需要确保添加了前面显示的适当的IF命令。

Windows 特有的功能

在 Windows 中构建图形应用程序时,能够控制是否显示控制台非常重要。这与将console添加到 QMake 项目的CONFIG变量中所解决的问题相同。


注意这里介绍的特定于 Windows 的解决方案可以与 MinGW 编译器一起使用,后者是开源版 Qt for Windows 附带的编译器。如果您使用另一个编译器,您将不得不修改该编译器的解决方案。


控制控制台可用性的方法是在链接时在windowsconsole子系统选项之间切换。将下面一行添加到您的CMakeLists.txt文件将给您一个没有控制台输出的应用程序:

SET( LINK_FLAGS -Wl,-subsystem,windows )

相反,使用控制台运行的应用程序是通过以下代码行实现的:

SET( LINK_FLAGS -Wl,-subsystem,console )

您还必须修改您的TARGET_LINK_LIBRARIES调用,以包含LINK_FLAGS变量,这将为您提供如下代码行:

TARGET_LINK_LIBRARIES( project ${QT_LIBRARIES} ${LINK_FLAGS} )

另一个需要解决的问题是应用程序图标。设置应用程序图标的实际操作是使用特殊的编译器从给定的 Windows 资源文件创建一个目标文件。下面的清单显示了 Windows 资源文件appicon.rc是如何编译成appicon.o的。然后,该文件被添加到项目源代码中,以便以后包含在实际的二进制文件中。

`ADD_CUSTOM_COMMAND(
  OUTPUT C M A K E C U R R E N T B I N A R Y D I R / a p p i c o n . o    C O M M A N D w i n d r e s . e x e      − I {CMAKE_CURRENT_BINARY_DIR}/appicon.o   COMMAND windres.exe     -I CMAKECURRENTBINARYDIR/appicon.o  COMMANDwindres.exe    I{CMAKE_CURRENT_SOURCE_DIR}
    -i${CMAKE_CURRENT_SOURCE_DIR}/appicon.rc
    -o ${CMAKE_CURRENT_BINARY_DIR}/appicon.o )

SET(project_SOURCES ${project_SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/appicon.o)`


注意CMake 命令可以分成几行,这就是为什么定制命令看起来很奇怪。


ADD_CUSTOM_COMMAND用于将定制构建方法插入到 CMake 生成的 Makefile 中。它由OUTPUT部分组成,列出了定制步骤生成的文件。在前面的清单中,输出是appicon.o文件。第二部分是COMMAND部分,指定要运行的实际命令。清单运行windres.exe文件,将-I-i-o命令行选项传递给它。

OS X 特有的特征

OS X 有一些特性,包括能够为 PowerPC 和 x86 平台使用相同的可执行二进制文件——一个通用的二进制文件。要创建这样的可执行文件,使用CMAKE_OSX_ARCHITECTURES变量并将其设置为ppc;i386:

SET( CMAKE_OSX_ARCHITECTURES ppc;i386 )


注意保持ppc;i386值一致很重要。不要添加空格。


要使用 CMake 设置应用程序图标,需要构建一个应用程序包,这并不像看起来那么难(CMake 处理大部分细节)。你所要做的就是设置一些值,然后在最后的构建阶段做一些调整。这些变量如下:

  • MACOSX_BUNDLE_ICON_FILE:要使用的图标文件(以icns文件格式)。
  • MACOSX_BUNDLE_BUNDLE_NAME:捆绑包的名称。
  • MACOSX_BUNDLE_COPYRIGHT:版权信息。
  • MACOSX_BUNDLE_INFO_STRING:信息字符串。
  • MACOSX_BUNDLE_GUI_IDENTIFIER:作为 Java 风格包名的唯一标识符。这意味着看起来像一个颠倒的 web 服务器名称,例如,se.thelins.exampleApplication就是这样一个字符串。
  • MACOSX_BUNDLE_BUNDLE_VERSION:版本字符串。
  • MACOSX_BUNDLE_SHORT_VERSION_STRING:短版本字符串。
  • MACOSX_BUNDLE_LONG_VERSION_STRING:长版本字符串。

在为这些字符串设置值之后,您必须告诉 CMake 在调用ADD_EXECUTABLE命令时创建一个包,方法是将下面一行添加到CMakeLists.txt文件中:

ADD_EXECUTABLE( exename MACOSX_BUNDLE ... )

特定于 Unix 和 X11 的特性

对于 Unix 系统,你需要让运行make install成为可能,所以 CMake 在安装之前必须知道要构建什么,要安装什么文件。例如,您不想将任何中间文件复制到安装目录中。

CMake 希望用户在运行 CMake 创建构建环境时指定CMAKE_INSTALL_PREFIX变量。它可能类似于下面的代码行,其中.。指的是CMakeLists.txt文件,/usr/local目录是安装目标:

cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local

可以安装两种类型的文件:目标文件和现有文件。目标是构建过程的结果。它们可以是名为RUNTIME的可执行文件,名为LIBRARY的动态链接库,名为ARCHIVE的静态链接库。使用ADD_EXECUTABLE命令创建RUNTIME目标。在本章的后面,你会学到如何创建库。

使用INSTALL命令来指定要安装的目标和安装位置。它可能看起来像这样:

INSTALL( TARGETS exenames   RUNTIME DESTINATION bin   LIBRARY DESTINATION lib )

exenames可以是目标名称的列表,包括可执行文件和任何类型的库。RUNTIME DESTINATION指定了RUNTIME目标相对于安装前缀的位置。本节前面的INSTALL命令结合cmake命令行会将这些文件放在/usr/local/bin目录中。LIBRARY DESTINATION以同样的方式工作。如果需要安装静态链接库,可以使用ARCHIVE DESTINATION指令来放置它们。您通常会从静态链接库构建可执行文件,这就是为什么我没有在前面的INSTALL命令中为它们指定目标目录。

前面提到了目标和现有文件。现有文件可以是文档文件、图标或任何其他不是在构建过程中生成的文件。要安装这些文件,结合使用FILES指令和INSTALL命令。语法如下所示:

INSTALL( FILES filesDESTINATIONdirectory )

在前面一行中,files表示源代码树中的文件列表。directory的指定与安装目标时的binlib相同。常见的目录是share/ appname,其中appname是应用程序的名称。

清单 15-10 显示了一个包含目标和文件的部分例子。

清单 15-10。 为安装设置文件

`SET( foo_DOCS docs/index.html docs/details.html )

ADD_EXECUTABLE( fooexe … )

INSTALL( TARGETS fooexe
  RUNTIME DESTINATION bin )
INSTALL( FILES ${foo_DOCS}
  DESTINATION share/foo/docs )`

使用 CMake 构建库

用 CMake 构建库真的很容易。您可以使用ADD_LIBRARY命令,而不是像构建应用程序时那样使用ADD_EXECUTABLE命令。要指定构建的是动态加载库还是静态库,请使用如下所示的SHAREDSTATIC指令:

ADD_LIBRARY( dllnameSHAREDdlldependencies) ADD_LIBRARY(libnameSTATIClibdependencies )

插件是一个共享库,但是建立在特定的环境中。这意味着在创建库目标之前,您必须使用ADD_DEFINITIONS命令向构建环境添加三个预处理器定义:

ADD_DEFINITIONS( -DQT_PLUGIN ) ADD_DEFINITIONS( -DQT_NO_DEBUG ) ADD_DEFINITIONS( -DQT_SHARED ) ADD_LIBRARY( pluginnameSHAREDplugindependencies )

添加的定义在发布模式下创建一个插件。如果不在发布模式下创建它,它将不会出现在设计器等工具中,因为它们是在发布模式下构建的。当在你的应用程序中使用插件时,规则是在发布和调试模式时要匹配应用程序和插件。


注意当然,添加的定义必须与您的 Qt 库的配置相匹配。如果你的 Qt 库是静态的,QT_SHARED不应该被定义。


使用 CMake 管理复杂项目

应用程序项目通常由多个组件组成。通常的设计由一个或多个用于构建一个或多个应用程序的库组成。建立什么依赖什么,建立这样一个系统不是一件简单的事情。

在这一节,你将使用来自清单 15-4 的项目,但是用 CMake 代替 QMake。CMake 设置的文件和目录如清单 15-11 所示。比较这两个清单可以发现,所有的 QMake 项目文件都被替换成了CMakeLists.txt文件。appbin目录也被替换为build目录,因为您将把构建过程放在源代码树之外。

清单 15-11。 复杂 CMake 项目中的文件和目录

|   CMakeLists.txt | +---build | +---app |   |   CMakeLists.txt |   |   appwindow.cpp |   |   appwindow.h |   |   main.cpp | +---include |       base.h | \---src     |  CMakeLists.txt     |  base.cpp

我们先来看一下CMakeLists.txt,它位于项目根目录下。您可以在清单 15-12 中看到整个文件,它从定义一个名为complex的项目开始。

项目命名之后的步骤将变量EXECUTABLE_OUTPUT_PATHLIBRARY_OUTPUT_PATH初始化到PROJECT_BINARY_DIR目录中的binlib目录。回想一下外源码构建对比内源码构建的解释:PROJECT_BINARY_DIR代表构建根目录。如果构建在源代码内部,它将与代表源代码根目录的PROJECT_SOURCE_DIR相同。

以下两个ADD_SUBDIRECTORIES命令构建了srcapp目录的内容(按此顺序):

清单 15-12。 根 CMake 文件

`PROJECT( complex )

SET( EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin )
SET( LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib )

ADD_SUBDIRECTORY( src )
ADD_SUBDIRECTORY( app )`

src目录下的CMakeLists.txt文件如清单 15-13 所示。整个文件遵循清单 15-8 中首次引入的模板,但是它的目标是一个静态库,而不是最后的应用程序。

当您使用 QMake 时,您可以设置一个依赖目录列表,在其中保存项目的源文件和头文件。因为使用 CMake 不容易做到这一点,所以您必须引用带有完整相对路径的base.h头文件:../include


注意在讨论 QMake 时,依赖目录通常(但不总是)与包含文件目录相同。


因为这个库是静态的,所以假设它通过它所链接的应用程序链接到 Qt。因此你不需要在这里添加一个TARGET_LINK_LIBRARIES命令。

LIBRARY_OUTPUT_PATH的值从根CMakeLists.txt文件保存到这个文件中(因为这个文件是从ADD_SUBDIRECTORIES命令调用的),所以结果文件将被放在正确的目录中。

清单 15-13。 用于构建静态库的 CMake 文件

`SET( src_SOURCES base.cpp )
SET( src_HEADERS …/include/base.h )

FIND_PACKAGE( Qt4 REQUIRED )
INCLUDE( ${QT_USE_FILE} )

INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/include )

QT4_WRAP_CPP( src_HEADERS_MOC ${src_HEADERS} )

ADD_DEFINITIONS( ${QT_DEFINITIONS} )

ADD_LIBRARY( base STATIC ${src_SOURCES} ${src_HEADERS_MOC} ).`

清单 15-14 显示了来自app目录的CMakeLists.txt文件。它很容易与清单 15-8 相比较,但是它有一些调整。

第一个是使用INCLUDE_DIRECTORIES命令添加公共的include目录。源文件需要这个命令来找到base.h文件。它还将 Qt 库旁边的base库添加到TARGET_LINK_LIBRARIES命令中的app目标中。

就像构建库一样,生成的可执行文件的位置由根CMakeLists.txt文件控制。使用由EXECUTABLE_OUTPUT_PATH指向的目录。

清单 15-14。 构建应用程序的 CMake 文件

`SET( app_SOURCES main.cpp appwindow.cpp )
SET( app_HEADERS appwindow.h )

FIND_PACKAGE( Qt4 REQUIRED )
INCLUDE( ${QT_USE_FILE} )

QT4_WRAP_CPP( app_HEADERS_MOC ${app_HEADERS} )

INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/include )

ADD_DEFINITIONS( ${QT_DEFINITIONS} )

ADD_EXECUTABLE( app ${app_SOURCES} ${app_HEADERS_MOC} )
TARGET_LINK_LIBRARIES( app base ${QT_LIBRARIES} )`

通过使用命令提示符进入build目录,然后运行cmake,引用根CMakeLists.txt文件,您将为整个项目生成 Makefiles。运行make现在可以构建所有的东西。在 MinGW 环境中运行它的输出显示在清单 15-15 中。可能的话,输出是彩色编码的。我突出显示了红色和紫色的线,表示一个构建的开始和这个构建的最终链接。

清单 15-15。 使用 CMake 和 MinGW 构建复杂项目

[ 14%] Generating moc_base.cxx Scanning dependencies of target base[ 28%] Building CXX object src/CMakeFiles/base.dir/base.obj [ 42%] Building CXX object src/CMakeFiles/base.dir/moc_base.objLinking CXX static library …/lib/libbase.a[ 42%] "Built target base" [ 57%] Generating moc_appwindow.cxxScanning dependencies of target app[ 71%] Building CXX object app/CMakeFiles/app.dir/main.obj [ 85%] Building CXX object app/CMakeFiles/app.dir/appwindow.obj [100%] Building CXX object app/CMakeFiles/app.dir/moc_appwindow.objLinking CXX executable …/bin/app.exe [100%] "Built target app"

总结

比较 QMake 和 CMake 很困难。这两种工具几乎可以做任何事情,并且都很成熟,但是它们的侧重点不同。QMake 使得为所有平台构建基于 Qt 的软件变得非常容易。CMake 也使它变得很容易,但是因为这个工具更通用,所以需要做的工作稍微多一点。

如果你计划使用非 Qt 组件或者参与 KDE 项目,那么推荐使用 CMake。否则,我建议您使用 QMake。

您可以构建应用程序、库(共享的和静态的)和插件,但是您必须注意一些特定于平台的细节。这些细节包括 Windows 和 OS X 的应用程序图标,OS X 的通用二进制文件和软件包,以及对于 Windows 平台,你是否想要一个控制台。

十六、单元测试

随着软件复杂性的增加和开发时间的不断缩短,开发人员不断寻找新的方法来更有效地创建和开发他们的应用程序。因为测试往往是一项消耗大量分配时间表的任务,所以对如何简化测试过程进行了大量的思考就不足为奇了。

作为这项工作的结果,一个常见的策略被称为单元测试,它是关于独立测试项目的所有部分,以确保它们按照规范工作。当把这些部分放在一起时,你会知道每个部分都像预期的那样工作,使得最终的测试和调试更加容易。

以一个单位转换应用程序为例,其中有数百个单位,甚至更多的情况需要测试。通过自动测试转换引擎单元和用户界面,您可以避免大量的测试。例如,测试用户界面可以提交值、源单元和目的单元就足够了;您不必从用户界面测试所有可能的转换。所有转换的可能性都将作为转换引擎测试的一部分进行测试。如果您遇到转换问题,您可以在测试转换引擎时发现它(您可以在不涉及用户界面的情况下调试它)。

测试可以根据应用程序中接口的规范来构建,从而确保规范得到满足。有些人甚至认为,测试产生了规范,应该在编写被测试的实际代码之前编写规范。

单元测试的概念最近受到了关注,因为它是敏捷软件开发概念的基础部分。单元测试使得实现功能的代码能够被改变。只要测试通过,代码仍然可以与应用程序的其余部分一起工作。这意味着你可以在任何时候修改你的代码,并且——假设测试都通过了——应用程序将继续按预期运行。这是敏捷软件开发的关键概念之一。


提示你可以在[www.agilemanifesto.org](http://www.agilemanifesto.org)[www.extremeprogramming.org](http://www.extremeprogramming.org)找到更多关于敏捷软件开发的信息。


单元测试可以被看作是对编译器和链接器的补充。这些工具可以在构建软件时发现明显的问题。内部问题——比如不起作用的堆栈、错误计算结果的函数等等——必须使用 beta 测试人员、单元测试或者(小心!)实际用户。通过使用单元测试,您可以确保您的 beta 测试人员关注重要的问题,并且您的用户不太可能在您的软件中发现错误。结果将是产品质量更好。

单元测试和 Qt

Qt 附带了一个轻量级的单元测试模块,即QtTest模块(这可能是意料之中的,因为 Qt 鼓励构建组件)。当使用这种方法进行开发时,能够单独测试每个组件是很重要的。

测试的结构

使用QtTest模块,每个单元测试都是从一个类中构造的,这个类必须继承QObject类并以Q_OBJECT宏开始。一个单元测试由几个测试用例组成,每个测试用例是一个私有槽。四个特殊插槽不被视为测试用例:

  • initTestCase:初始化单元测试类,在测试用例运行前调用。
  • cleanupTestCase:清理单元测试,并在所有测试用例运行后被调用。
  • init:这个方法在每个测试用例之前运行。
  • cleanup:这个方法在每个测试用例之后运行。

所有其他插槽都被视为测试用例,并相应地运行。执行顺序,包括之前列出的特殊槽,可以在图 16-1 中看到。

每个测试用例的目的是测试一个类的一个或多个方面。例如,您可能会测试一个函数,使它总是执行正确的计算,或者您可能会测试一个接口,以确保对象的内部状态按预期运行。

在这两种情况下,测试常见情况和边缘情况都很重要。验证常见情况的测试可能很少,但是它们应该确保大多数使用的单元功能正常工作。测试还必须包括处理错误的用户输入。例如,当用户输入无效输入时,可能会返回空字符串或发出警告消息。边界情况确保函数实际执行,甚至在靠近面向用户的边界时(例如,确保列表的两端都是可访问的,或者用户可以在输入字段中输入任意大的值,而且数学函数可以处理其函数的所有极值点,甚至是可以传递给它的最大可能数字)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16-1。 单元测试运行时的执行顺序

清单 16-1 展示了实现测试的类的基本结构,以及使用特殊单元测试main函数运行实际测试的QTEST_MAIN宏。main函数宏可以放在任何地方——甚至放在与测试类不同的文件中。

清单 16-1。 一个单元测试的基本结构

class MyTestClass : public QObject

{

  Q_OBJECT

private slots:

  // Test cases goes here

};

...

QTEST_MAIN( DateTest )

测试用例的项目文件需要包括被测试的类、测试类和一个配置行CONFIG += qtestlib。可以通过运行qmake -project CONFIG+=qtestlib来创建这样一个文件。下面我们来详细看一下。

对于 Qt 来说,测试实际上只是应用程序,所以项目文件以app模板开始(您也可以使用标准的包含和依赖路径):

TEMPLATE = app

INCLUDEPATH = .

DEPENDPATH = .
然后,为目标应用程序命名:
TARGET = mytestapp
接下来是被测试的类——包括头文件和源代码:
HEADERS += myclass.h

SOURCES += myclass.cpp
然后是测试类——头和源——以及包含`main`函数的`main.cpp`文件:
HEADERS += mytestclass.h

SOURCES += mytestclass.cpp main.cpp
最后,配置行:
CONFIG += qtestlib

**注意**测试结果输出到控制台;在 Windows 平台上,您还必须在项目文件中添加一行`CONFIG += console`。

因为测试是一个普通的应用程序,所以您需要做的就是运行`qmake && make`来构建它。然后您可以运行产生的`mytestapp`来执行测试。
测试日期
让我们使用`QtTest`模块来测试一个数据类。对于这个测试,您将使用`QDate`类,因为它有一个内部状态,因为它以某种方式向自己表示日期。它还有一个由`isValid`、`day`、`month`、`year`属性 getters 组成的接口;以及从`addDays`、`addMonths`和`addYears`方法。
那么应该测试什么呢?可以给日期添加日、月和年。添加日期可以更改日期的日、月和年。添加月份只会修改月份和年份,而添加年份只会影响`year`属性。我还喜欢测试日期是否有效(229 日在闰年有效,但在其他年份无效)。
实施测试
所有这些测试都在清单 16-2 所示的单元测试类中实现。该类继承了`QObject`并包含了`Q_OBJECT`。然后,不同的测试被实现为私有插槽。请注意,特殊的插槽已经被省略了,因为您不需要进行任何特殊的初始化或清理。
测试分为`testAddDays`、`testAddMonths`、`testAddYears`和`testValid`。前三个测试增加日、月、年;最后一项测试检查`isValid`方法是否正常工作。

**清单 16-2***`DateTest`*类包含* `QDate` *类的测试。** *`class DateTest : public QObject

{

  Q_OBJECT

private slots:

  void testAddDay();

  void testAddMonth();

  void testAddYear();

  void testValid();

};` 

从底部开始,看一下`testValid`方法(它的实现如清单 16-3 所示)。测试从设置日期开始,然后测试`QVERIFY`宏,看看`isValid`方法是否返回预期值。

`QVERIFY(bool)`宏是`QtTest`模块的一部分,用于验证给定的表达式是否为`true`。如果您想在表达式为`false`时关联一个特定的错误消息,您可以使用`QVERIFY2(bool,string)`宏,它会在出现问题时打印字符串。

一旦一个测试宏失败,当前的测试用例就会被中止,所以你不必担心将来的宏会因为第一个问题而失败。如果您需要清理任何东西,请在特殊的`cleanup`槽中进行。

第一个测试检查未指定的日期是否无效,有效的日期是否有效。所以 229 日在 1980(闰年)有效,但在 1979 年无效。

**清单 16-3** *测试* `Valid` *方法是否按预期工作*

void DateTest::testValid()

{

QDate date;

QVERIFY( !date.isValid() );

date = QDate( 1979, 5, 16 );

QVERIFY( date.isValid() );

date = QDate( 1980, 2, 29 );

QVERIFY( date.isValid() );

date = QDate( 1979, 2, 29 );

QVERIFY( !date.isValid() );

}


也可以使用`QVERIFY`来检查数值。例如,`QVERIFY(x==4)`检查`x`是否等于`4`。另一种选择是改为写`QCOMPARE(x,4)`。这使用了`QCOMPARE`宏来查看实际值`x`是否等于期望值`4`。好处是测试失败时返回的消息告诉您实际值和期望值。

清单 16-4 显示了运行中的`QCOMPARE`宏。显示的时间段`testAddMonths`从设置日期开始。然后,它给给定的日期加上一个月,并确保日期的月份部分得到正确更新。然后给日期加上 12 个月,看到数据的年份部分也有效。

**清单 16-4** *添加月份并检查结果*

void DateTest::testAddMonth()

{

QDate date( 1973, 8, 16 );

QCOMPARE( date.year(), 1973 );

QCOMPARE( date.month(), 8 );

QCOMPARE( date.day(), 16 );

QDate next = date.addMonths( 1 );

QCOMPARE( next.year(), 1973 );

QCOMPARE( next.month(), 9 );

QCOMPARE( next.day(), 16 );

next = date.addMonths( 12 );

QCOMPARE( next.year(), 1974 );

QCOMPARE( next.month(), 8 );

QCOMPARE( next.day(), 16 );

}


`testAddDays`和`testAddYears`插槽看起来非常像`testAddMonths`插槽。年份测试槽简单地增加了一些年份。这是唯一的测试案例,因为添加的年数只影响返回的年份。然而,添加天数的测试有三种情况:添加一天(只影响`day`属性)、添加 31(影响`month`属性)和添加 366(影响`year`属性)**组装在一起**

`DateTest`类保存在`datetest.cpp`和`datetest.h`文件中。要创建一个应用程序,你必须添加一个`main`函数,它保存在清单 16-5 所示的`main.cpp`文件中。

首先包含的`QtTest`头包含来自`QtTest`模块的所有宏(包括`QVERIFY`、`QCOMPARE`等等)。下一行包括实现实际测试的类。然后,`QTEST_MAIN`宏创建一个运行测试用例的`main`函数。

**清单 16-5** *使用* `QTEST_MAIN` *宏实现* `main` *功能。*

#include

#include “datetest.h”

QTEST_MAIN( DateTest )


这些都是从一个项目文件中引用的,该文件是通过调用`qmake –project "CONFIG+=qtestlib console"`自动生成的。`qtestlib`引用添加了对`QtTest`模块的引用,而`console`是 Windows 用户所必需的。没有它,就不会显示任何消息。结果文件如清单 16-6 所示。

**清单 16-6** *项目文件把这一切联系在一起*

######################################################################

Automatically generated by qmake (2.01a) ti 23. jan 18:26:56 2007

######################################################################

TEMPLATE = app

TARGET =

DEPENDPATH += .

INCLUDEPATH += .

Input

HEADERS += datetest.h

SOURCES += datetest.cpp main.cpp

CONFIG += qtestlib console


当所有文件都准备好了,接下来就是构建和执行测试了。

**运行测试**

构建单元测试的结果是一个普通的应用程序。如果你在没有任何命令行参数的情况下运行该应用程序,它将产生类似于清单 16-7 的结果。输出显示了 Qt 的版本和使用的`qtestlib`的版本,后面是每个测试用例的结果。这种情况下,都得到一个`PASS`,最后的总结显示所有测试都通过了。

* * *

**提示**如果你想要彩色输出,设置环境变量`QTEST_COLORED`为 1* * *

**清单 16-7** *不带任何参数运行测试*

********* Start testing of DateTest *********

Config: Using QTest library 4.2.2, Qt 4.2.2

PASS   : DateTest::initTestCase()

PASS   : DateTest::testAddDay()

PASS   : DateTest::testAddMonth()

PASS   : DateTest::testAddYear()

PASS   : DateTest::testValid()

PASS   : DateTest::cleanupTestCase()

Totals: 6 passed, 0 failed, 0 skipped

********* Finished testing of DateTest *********


有时测试用例会挂起。当这种情况发生时,在执行测试应用程序时使用`–v1`命令行参数是很方便的。当给出这个标志时,输出会告诉您每个测试是何时进入并通过的,因此您可以知道测试在哪里挂起。清单 16-8 中的显示了一个输出片段。

**清单 16-8** *运行测试用* `-v1` *标志*

********* Start testing of DateTest *********

Config: Using QTest library 4.2.2, Qt 4.2.2

INFO   : DateTest::initTestCase() entering

PASS   : DateTest::initTestCase()

INFO   : DateTest::testAddDay() entering

PASS   : DateTest::testAddDay()

INFO   : DateTest::testAddMonth() entering

PASS   : DateTest::testAddMonth()

INFO   : DateTest::testAddYear() entering


如果在定位挂起时仍然有问题,或者只是想确保所有测试都运行了,那么可以使用`–v2`参数,它会在每个测试进入并通过时产生测试输出(就像使用`-v1`时一样),但是它也会在每个测试宏到达时显示出来。清单 16-9 展示了这一点。每个宏都有一行告诉你它的位置——读起来像这样:`filename.ext (line) : failure location`。

**清单 16-9** *运行测试用* `-v2` *标志*

********* Start testing of DateTest *********

Config: Using QTest library 4.2.2, Qt 4.2.2

INFO   : DateTest::initTestCase() entering

PASS   : DateTest::initTestCase()

INFO   : DateTest::testAddDay() entering

INFO   : DateTest::testAddDay() COMPARE()

datetest.cpp(10) : failure location

INFO   : DateTest::testAddDay() COMPARE()

datetest.cpp(11) : failure location

INFO   : DateTest::testAddDay() COMPARE()

datetest.cpp(12) : failure location

INFO   : DateTest::testAddDay() COMPARE()


当一个测试失败时,当前测试用例立即停止。导致失败的宏将会报告出了什么问题以及它的位置,就像对`–v2`标志一样。在清单 16-10 中可以看到一个失败的例子。输出来自没有任何命令行参数的测试。

如果一个测试用例失败了,其他的仍然会运行,因此您可以获得一个完整的测试状态。

**清单 16-10** *一次考试失败。*

********* Start testing of DateTest *********

Config: Using QTest library 4.2.2, Qt 4.2.2

PASS   : DateTest::initTestCase()

PASS   : DateTest::testAddDay()

FAIL!  : DateTest::testAddMonth() Compared values are not the same

Actual (next.day()): 16

Expected (15): 15

datetest.cpp(43) : failure location

PASS   : DateTest::testAddYear()

PASS   : DateTest::testValid()

PASS   : DateTest::cleanupTestCase()

Totals: 5 passed, 1 failed, 0 skipped

********* Finished testing of DateTest *********


失败的原因是`QCOMPARE`宏中的期望值在`datetest.cpp`的第 43 行被更改。

如果您想将测试限制在一个测试用例中,您可以将插槽的名称作为命令行参数传递。例如,运行`datetest testValid`只运行`testValid`测试用例。

#### 数据驱动测试

在`DateTest`中实现的测试有很多重复的代码。例如,清单 16-4 中的`testAddMonths`方法添加了一个日期并检查结果两次。`testAddDays`三次添加天数,`testValid`以同样的方式测试三个日期。

所有这些代码重复鼓励复制粘贴编程,从而导致错误。为了避免重复,您可以将测试用例设计成数据驱动的。简单地说,就是把数据放到一个通常被称为*测试向量*的表格中。然后,对表中的每一行执行相同的测试。尽管自己实现这一点可能很容易,但由于场景非常常见,因此`QtTest`模块提供了内置支持。

为了让`QtTest`模块为您处理数据服务细节,您必须实现一个特定的结构。对于每个数据驱动的测试用例槽,您需要一个同名的槽,但是以`_data`结尾,它为那个测试用例生成数据。清单 16-11 显示`testAddDays`、`testAddMonths`和`testAddYears`已经合并到`testAdd`插槽中。该插槽从`testAdd_data`插槽接收数据。对于从`testValid_data`获取数据的`testValid`插槽也是如此。可能有一个或多个数据驱动的测试用例与非数据驱动的测试用例在同一个类中,但是在这种情况下,所有的测试(或多或少)都是数据驱动的。

**清单 16-11** *数据驱动* `DateTest` **

class DateTest : public QObject

{

Q_OBJECT

private slots:

void testAdd();

void testAdd_data();

void testValid();

void testValid_data();

};


新的`testValid`槽及其数据槽如清单 16-12 所示。让我们从查看`testValid_data`数据槽开始。它首先用`QTest::addColumn<type>: year, month, day`和`valid`创建四列,其中`valid`是您期望`isValid`方法为由`year`、`month`和`day`组成的日期返回的值。然后使用`QTest::newRow`方法添加数据行。每一行都有一个名称,然后使用< <操作符输入列的数据。

通过使用`QFETCH`宏获取`testValid`测试用例槽以及`year`、`month`和`day`值。请注意,`testValid`只知道有哪些列,并且有一个当前行。有多少行以及哪一行现在是活动的并不重要;`QtTest`模块确保该槽为每行数据调用一次。

`QFETCH`宏有两个参数:要获取的数据类型和要获取的列名。该值可从具有列名的变量中获得,这就是为什么您可以将`QDate`构造器中的`year`、`month`和`day`用作普通变量。

可以使用`QFETCH`宏从`value`列获得值,然后使用`QCOMPARE`甚至`QVERIFY`来检查它是否与预期值匹配。然而,代替这样做,你可以马上使用`QTEST`宏。它的工作方式类似于`QCOMPARE`,但是它接受一个列名,而不是一个期望值。然后,它将给定值与当前数据行的给定列的值进行比较。

* * *

**注意**在将`testValid`变成数据驱动测试用例的过程中,丢失了对一个空构造器的检查。

* * *

**清单 16-12** *检查日期范围是否有效*

void DateTest::testValid()

{

QFETCH( int, year );

QFETCH( int, month );

QFETCH( int, day );

QDate date( year, month, day );

QTEST( date.isValid(), “valid” );

}

void DateTest::testValid_data()

{

QTest::addColumn( “year” );

QTest::addColumn( “month” );

QTest::addColumn( “day” );

QTest::addColumn( “valid” );

QTest::newRow( “Valid, normal” ) << 1973 << 8 << 16 << true;

QTest::newRow( “Invalid, normal” ) << 1973 << 9 << 31 << false;

QTest::newRow( “Valid, leap-year” ) << 1980 << 2 << 29 << true;

QTest::newRow( “Invalid, leap-year” ) << 1981 << 2 << 29 << false;

}


`testAdd`槽的变化比`testValid`稍大。(该插槽及其附带的数据插槽可以在清单 16-13 中看到。)数据分为六列:`addDay`、`addMonth`、`addYear`、`day`、`month`和`year`。测试用例的工作方式是获取一个预先确定的日期(在本例中是 1979516),然后向其中添加`addXxx`列。日、月和年列用于保存预期的结果。

正如您在`testAdd`槽实现中看到的,使用`QFETCH`来检索`addXxx`值。然后使用`QTEST`宏检查结果日期。在`testAdd_data`槽中创建的数据对应于在非数据驱动类的`testAddXxx`方法中执行的测试。

**清单 16-13** *检查*`addDays``addMonths`*`addYears`*方法是否按预期工作**

void DateTest::testAdd()

{

QDate date( 1979, 5, 16 );

QFETCH( int, addYear );

QFETCH( int, addMonth );

QFETCH( int, addDay );

QDate next = date.addYears( addYear ).addMonths( addMonth ).addDays( addDay );

QTEST( next.year(), “year” );

QTEST( next.month(), “month” );

QTEST( next.day(), “day” );

}

void DateTest::testAdd_data ()

{

QTest::addColumn( “addYear” );

QTest::addColumn( “addMonth” );

QTest::addColumn( “addDay” );

QTest::addColumn( “year” );

QTest::addColumn( “month” );

QTest::addColumn( “day” );

QTest::newRow( “Start date” )    << 0 << 0 << 0 << 1979 << 5 << 16;

}


项目的其余部分不需要更新,数据驱动版本的`DateTest`就可以工作。从命令行运行测试时看到的结果也是相似的。实际的测试用例在运行时被列出,而数据槽被忽略。

使用数据驱动测试的一个有趣的副作用是,当测试失败时,返回每行数据的名称(使错误消息更加清晰)。在清单 16-14 中你可以看到一个这样的例子。不要只是说`next.year()`值是意外的,你要知道测试用例是`testAdd(Twenty days)`。

**清单 16-14** *当一个测试在数据驱动的测试用例中失败时,当前行的名称作为失败消息的一部分给出。*

********* Start testing of DateTest *********

Config: Using QTest library 4.2.2, Qt 4.2.2

PASS   : DateTest::initTestCase()

FAIL!  : DateTest::testAdd(Twenty days) Compared values are not the same

Actual (next.year()): 1979

Expected (“year”): 2979

datetest.cpp(18) : failure location

PASS   : DateTest::testValid()

PASS   : DateTest::cleanupTestCase()

Totals: 3 passed, 1 failed, 0 skipped

********* Finished testing of DateTest *********


下面的列表总结了转向数据驱动测试的结果:** 

*** 更少的代码:您只需要实现一次测试,但是使用那个测试运行不同的情况。* 更少的代码冗余:因为测试只实现一次,所以不会重复。这也意味着如果有问题,不必修复所有测试中的错误。* 潜在的更好的失败消息:因为每个测试向量行都有一个名称,您可以清楚地看到哪个案例失败了。* 一些测试用例不能再被执行:这是一个缺点。因为测试向量总是包含数据,所以很难用它来测试一些特殊的情况(例如,一个空的构造器)。这将要求您在测试代码中有一个特例和一个表示没有数据的标志,这会使测试代码混乱。**

**最后一点可以通过将这些测试放在非数据驱动的测试用例中来解决。这不是一个限制,因为它们可以在一个类中与数据驱动测试相结合。

测试小工具

用自动化测试(比如单元测试)很难检查的一个方面是用户交互。虽然大多数小部件都有可以测试的 setters 和 getters,但是为了测试用户交互,您必须能够模拟鼠标和键盘活动。QtTest模块可以提供帮助。

测试旋转盒

为了测试一个小部件,您将对QSpinBox类进行测试,重点是上下改变值的能力以及最小值和最大值是否被考虑。因为这个值可以用三种不同的方式改变,所以清单 16-15 中的测试类包含了三个测试用例槽:

  • testKeys:测试使用键盘交互改变数值
  • testClicks:使用鼠标交互改变值的测试
  • testSetting:使用setValue方法改变数值的测试

测试小部件和非小部件的单元测试类之间没有区别。

**清单 16-15。**一级用于测试 QSpinBox一级一级

class SpinBoxTest : public QObject

{

  Q_OBJECT

private slots:

  void testKeys();

  void testClicks();

  void testSetting();

};

你要考虑的第一个测试用例是testSetting插槽,如清单 16-16 所示。在这个测试用例中,被测试的类是小部件并不重要;您只需测试 value 属性。首先创建一个QSpinBox对象;随后,其范围被设置为 1–10。

然后,测试会尝试设置一个有效值,设置一个过小的值,最后设置一个过大的值。有效值应该保持不变,而其他两个值应该保持在指定的范围内。

清单 16-16。 使用编程接口测试 value 属性

void SpinBoxTest::testSetting()

{

  QSpinBox spinBox;

  spinBox.setRange( 1, 10 );

  spinBox.setValue( 5 );

  QCOMPARE( spinBox.value(), 5 );

  spinBox.setValue( 0 );

  QCOMPARE( spinBox.value(), 1 );

  spinBox.setValue( 11 );

  QCOMPARE( spinBox.value(), 10 );

}

清单 16-17 显示了第一个交互测试:testKeys。测试从创建一个QSpinBox开始,并设置与testSetting测试相同的范围。然后,在按下向上和向下键之前,数字显示框被初始化为有效值。在每次按键之间测试这些值,因此value属性会按预期改变。接下来的两个测试将该值设置为一个极限值,并通过按键尝试移动到允许的范围之外。在这里,您要确保value属性不会改变。

使用QTest::keyClick(QWidget*,Qt::Key)方法将按键发送到数字显示框。通过使用keyClick向小部件发送一个键事件,Qt 自动为该键发送一个keyPress事件和一个keyRelease事件。

清单 16-17。 测试改变 value 使用键盘交互

void SpinBoxTest::testKeys()

{

  QSpinBox spinBox;

  spinBox.setRange( 1, 10 );

  spinBox.setValue( 5 );

  QTest::keyClick( &spinBox, Qt::Key_Up );

  QCOMPARE( spinBox.value(), 6 );

  QTest::keyClick( &spinBox, Qt::Key_Down );

  QCOMPARE( spinBox.value(), 5 );

  spinBox.setValue( 10 );

  QTest::keyClick( &spinBox, Qt::Key_Up );

  QCOMPARE( spinBox.value(), 10 );

  spinBox.setValue( 1 );

  QTest::keyClick( &spinBox, Qt::Key_Down );

  QCOMPARE( spinBox.value(), 1 );

}

void SpinBoxTest::testClicks()

{

  QSpinBox spinBox;

  spinBox.setRange( 1, 10 );

  spinBox.setValue( 5 );

  QSize size = spinBox.size();

  QPoint upButton = QPoint( size.width()-2, 2 );

  QPoint downButton = QPoint( size.width()-2, size.height()-2 );

  QTest::mouseClick( &spinBox, Qt::LeftButton, 0, upButton );

  QCOMPARE( spinBox.value(), 6 );

  QTest::mouseClick( &spinBox, Qt::LeftButton, 0, downButton );

  QCOMPARE( spinBox.value(), 5 );

  spinBox.setValue( 10 );

  QTest::mouseClick( &spinBox, Qt::LeftButton, 0, upButton );

  QCOMPARE( spinBox.value(), 10 );

  spinBox.setValue( 1 );

  QTest::mouseClick( &spinBox, Qt::LeftButton, 0, downButton );

  QCOMPARE( spinBox.value(), 1 );

}

void SpinBoxTest::testSetting()

{

  QSpinBox spinBox;

  spinBox.setRange( 1, 10 );

  spinBox.setValue( 5 );

  QCOMPARE( spinBox.value(), 5 );

  spinBox.setValue( 0 );

  QCOMPARE( spinBox.value(), 1 );

  spinBox.setValue( 11 );

  QCOMPARE( spinBox.value(), 10 );

}

最后一个测试槽检查鼠标交互。测试与前两个测试用例相同:尝试在有效范围内移动;然后试着向外移动。你可以在清单 16-18 中的槽中看到它的实现。

testClicks槽与testKeys槽非常相似,除了不是按键点击,而是发送鼠标点击,鼠标点击必须对准部件上的一个点。三条突出显示的线计算了向上和向下按钮的位置。看看这些线条和图 16-2 ,它显示了正在测试的小部件。

使用QTest::mouseClick(QWidget*, Qt::MouseButton, Qt::KeyboardModifiers, QPoint)方法将鼠标点击发送到小部件。清单中使用的参数模拟在没有任何键盘修饰键(Shift、Alternate、Ctrl 等)活动的情况下单击鼠标左键。单击的点取决于您是尝试单击向上还是向下按钮。


注意使用的点期望向上和向下按钮像在 Windows XP 风格中那样出现。更改样式或使用从右向左的布局会导致测试停止工作。


清单 16-18。 测试使用鼠标交互改变value

void SpinBoxTest::testClicks()

{

  QSpinBox spinBox;

  spinBox.setRange( 1, 10 );

  spinBox.setValue( 5 );

  QSize size = spinBox.size();

  QPoint upButton = QPoint( size.width()-2, 2 );

  QPoint downButton = QPoint( size.width()-2, size.height()-2 );

  QTest::mouseClick( &spinBox, Qt::LeftButton, 0, upButton );

  QCOMPARE( spinBox.value(), 6 );

  QTest::mouseClick( &spinBox, Qt::LeftButton, 0, downButton );

  QCOMPARE( spinBox.value(), 5 );

  spinBox.setValue( 10 );

  QTest::mouseClick( &spinBox, Qt::LeftButton, 0, upButton );

  QCOMPARE( spinBox.value(), 10 );

  spinBox.setValue( 1 );

  QTest::mouseClick( &spinBox, Qt::LeftButton, 0, downButton );

  QCOMPARE( spinBox.value(), 1 );

}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16-2。 一个数字显示框小工具

QTEST_MAIN function 宏将旨在测试小部件的单元测试和测试应用程序其他方面的单元测试同等对待。项目文件也不需要更改。通过构建和运行前面显示的单元测试,您可以获得一个通过测试用例的列表。

用数据驱动小部件

您遇到了与QDate类相同的冗余问题——QSpinBox的单元测试包含大量重复代码。解决方案是将测试转换成数据驱动的测试,这是以完全相同的方式完成的——不管被测试的是什么类。

所有测试用例都以相似的方式转换,所以从关注testKeys槽开始。在清单 16-19 中,插槽的新版本与testKeys_data一起显示。

清单中显示的大部分源代码应该是清晰的。但是,突出显示的两行很重要。当添加类型为Qt::Key的列时,如果没有将其声明为元类型,就会出现编译错误。使用Q_DECLARE_METATYPE宏进行注册。

测试用例像所有数据驱动测试一样工作:它使用QFETCH获取数据,并在使用QTEST检查测试结果之前使用这些数据。

清单 16-19。 使用数据驱动测试用例测试键盘交互

``Q_DECLARE_METATYPE( Qt::Key )`

void SpinBoxTest::testKeys()
{
  QSpinBox spinBox;
  spinBox.setRange( 1, 10 );

QFETCH( Qt::Key, key );
  QFETCH( int, startValue );

spinBox.setValue( startValue );
  QTest::keyClick( &spinBox, key );
  QTEST( spinBox.value(), “endValue” );
}

void SpinBoxTest::testKeys_data()
{
  QTest::addColumn<Qt::Key>( "key" );
  QTest::addColumn( “startValue” );
  QTest::addColumn( “endValue” );

QTest::newRow( “Up” ) << Qt::Key_Up << 5 << 6;
  QTest::newRow( “Down” ) << Qt::Key_Down << 5 << 4;
  QTest::newRow( “Up, limit” ) << Qt::Key_Up << 10 << 10;
  QTest::newRow( “Down, limit” ) << Qt::Key_Down << 1 << 1;
}

void SpinBoxTest::testClicks()
{
  QSpinBox spinBox;
  spinBox.setRange( 1, 10 );
  QSize size = spinBox.size();
  QPoint upButton = QPoint( size.width()-2, 2 );
  QPoint downButton = QPoint( size.width()-2, size.height()-2 );

QFETCH( QString, direction );
  QFETCH( int, startValue );

spinBox.setValue( startValue );

if( direction.toLower() == “up” )
    QTest::mouseClick( &spinBox, Qt::LeftButton, 0, upButton );
  else if (direction.toLower() == “down” )
    QTest::mouseClick( &spinBox, Qt::LeftButton, 0, downButton );
  else
    QWARN( “Unknown direction - no clicks issued.” );

QTEST( spinBox.value(), “endValue” );
}

void SpinBoxTest::testClicks_data()
{
  QTest::addColumn( “direction” );
  QTest::addColumn( “startValue” );
  QTest::addColumn( “endValue” );

QTest::newRow( “Up” ) << “Up” << 5 << 6;
  QTest::newRow( “Down” ) << “Down” << 5 << 4;
  QTest::newRow( “Up, limit” ) << “Up” << 10 << 10;
  QTest::newRow( “Down, limit” ) << “Down” << 1 << 1;
}

void SpinBoxTest::testSetting()
{
  QSpinBox spinBox;
  spinBox.setRange( 1, 10 );

QFETCH( int, value );

spinBox.setValue( value );
  QTEST( spinBox.value(), “endValue” );
}

void SpinBoxTest::testSetting_data()
{
  QTest::addColumn( “value” );
  QTest::addColumn( “endValue” );
  QTest::newRow( “Valid” ) << 5 << 5;
  QTest::newRow( “Over” ) << 11 << 10;
  QTest::newRow( “Under” ) << 0 << 1;
}`

testClicks槽类似于testKeys槽,但是您不能添加一个列来容纳要点击的QPoint,因为该点是在您知道被测试的小部件的大小时计算的。已经添加了一个名为direction的列。方向可以是"Up""Down"(见清单 16-20 )。

测试用例槽按预期工作:它设置QSpinBox,使用QFETCH获取输入数据,根据数据执行任务,然后使用QTEST进行评估。新的是,如果它运行在一个意想不到的方向,它使用QWARN宏通知用户。此警告不影响测试结果;它只是在日志中发出一个警告。

清单 16-20。 使用数据驱动测试用例测试鼠标交互

void SpinBoxTest::testClicks()

{

  QSpinBox spinBox;

  spinBox.setRange( 1, 10 );

  QSize size = spinBox.size();

  QPoint upButton = QPoint( size.width()-2, 2 );

  QPoint downButton = QPoint( size.width()-2, size.height()-2 );

  QFETCH( QString, direction );

  QFETCH( int, startValue );

  spinBox.setValue( startValue );

  if( direction.toLower() == "up" )

    QTest::mouseClick( &spinBox, Qt::LeftButton, 0, upButton );

  else if (direction.toLower() == "down" )

    QTest::mouseClick( &spinBox, Qt::LeftButton, 0, downButton );

  else

    QWARN( "Unknown direction - no clicks issued." );

  QTEST( spinBox.value(), "endValue" );

}

void SpinBoxTest::testClicks_data()

{

  QTest::addColumn<QString>( "direction" );

  QTest::addColumn<int>( "startValue" );

  QTest::addColumn<int>( "endValue" );

  QTest::newRow( "Up" ) << "Up" << 5 << 6;

...

}

textSetting槽以类似的方式转换,此处未示出。单元测试的结果也没有改变。测试以同样的方式进行(并给出结果)。

测试信号

Qt 类在受到编程调用或用户交互的刺激时会发出信号。因为信号和插槽是 Qt 应用程序的关键组件,所以在测试过程中不能忽略它们。

您可以使用QSignalSpy类来监听信号,而无需连接到它们。信号间谍被连接起来监听来自某个物体的某个信号。然后,spy 对象记录每个被捕获信号的参数值。

清单 16-21 显示了数据驱动的testKeys方法扩展了信号监听功能。(最初的实现槽显示在清单 16-19 中。)

清单中突出显示的行显示了该插槽的主要新增内容。从上到下查看变化,第一行创建了一个QSignalSpy对象,用于监视从spinBox对象发出的valueChanged(int)信号。信号 spy 是在数字显示框设置了起始值后创建的,以避免误捕捉信号。


注意这个测试只检查一个信号。在现实生活中,你也会包括valueChanged(QString)信号。


当间谍被创建时,实际的测试正在被执行。测试完成后,获取新列willSignal的值。如果该值为true,则预期有信号。

如果一个信号是预期的,核实间谍已经捕捉到一个信号。在了解如何做到这一点之前,您必须理解QSignalSpy继承了QList<QList<QVariant> >。这意味着它是一个包含变量对象的列表列表。

使用count属性检查捕获的信号数量。要从信号的第一个参数中获取值,使用takeFirst方法获取信号的参数值列表。返回的列表的第零个索引(即信号的第一个参数)在与预期的最终值进行比较之前,使用toIntQVariant转换为一个整数。

如果willSignal告诉您没有预期的信号,请确认没有发出信号。很容易忘记检查无信号情况。如果你错过了它,并且一个信号没有改变地被发射,两个互相连接的物体将会在一个无限循环中挂起。

对测试用例数据槽的改变被限制在新的列willSignal中,该列保存一个布尔值,告诉测试是否期望一个信号。

清单 16-21。 测试键盘交互——现在增加了额外的信号监控技能

void SpinBoxTest::testKeys()

{

  QSpinBox spinBox;

  spinBox.setRange( 1, 10 );

  QFETCH( Qt::Key, key );

  QFETCH( int, startValue );

  spinBox.setValue( startValue );

  QSignalSpy spy( &spinBox, SIGNAL(valueChanged(int)) );

  QTest::keyClick( &spinBox, key );

  QTEST( spinBox.value(), "endValue" );

  QFETCH( bool, willSignal );

  if( willSignal )

  {

    QCOMPARE( spy.count(), 1 );

    QTEST( spy.takeFirst()[0].toInt(), "endValue" );

  }

  else

    QCOMPARE( spy.count(), 0 );

}

void SpinBoxTest::testKeys_data()

{

  QTest::addColumn<Qt::Key>( "key" );

  QTest::addColumn<int>( "startValue" );

  QTest::addColumn<int>( "endValue" );

  QTest::addColumn<bool>( "willSignal" );

  QTest::newRow( "Up" ) << Qt::Key_Up << 5 << 6 << true;

  QTest::newRow( "Down" ) << Qt::Key_Down << 5 << 4 << true;

  QTest::newRow( "Up, limit" ) << Qt::Key_Up << 10 << 10 << false;

  QTest::newRow( "Down, limit" ) << Qt::Key_Down << 1 << 1 << false;

}

对另外两个测试用例槽testClickstestSetting的修改几乎与对testKeys的修改相同。最大的变化是不得不用一个startValue列和一个测试无信号情况的新测试用例来扩展testSetting

对测试的更改仅限于添加一个新对象。然后使用来自QtTest模块的标准宏检查该对象的状态。这意味着该装置的制造和使用方式与不检查信号的测试完全相同。

真实测试

到目前为止,您只测试了 Qt 附带的类的部分接口。现在你将为第十三章中的ImageCollection类创建一个单元测试。

界面

在看单元测试类之前,让我们快速回顾一下ImageCollection类,它用于保存图像和标签。可以添加新图像、为图像添加标签、检索所有标签、检索与一组标签匹配的图像的所有 id,以及从 id 中获取特定图像。可用的方法如下所示:

  • QImage getImage(int id):从给定的 id 中获取图像。
  • QList <int> getIds(QStringList tags):检索与任何指定标签匹配的图像的 id。如果没有指定标记,该方法将返回所有 id。
  • QStringList getTags():检索所有标签的列表。
  • addTag(int id, QString tag):给给定图像添加标签。
  • addImage(QImage image, QStringList tags):用给定的标签将图像添加到集合中。
测试

为了测试这些方法,将测试分为三个部分:一个用于测试标签,一个用于测试图像,一个用于测试来自标签关联的图像。这三个部分可以看作是单元测试类声明中的槽,如清单 16-22 所示。

该类包含一个名为pixelCompareImages的私有成员函数。它用于确保两幅图像完全相同,一个像素一个像素。需要查看图像是否正确存储在数据库中。

清单 16-22。 单元测试类用于测试 ImageCollection

class ImageCollectionTest : public QObject

{

  Q_OBJECT

private slots:

  void testTags();

  void testImages();

  void testImagesFromTags();

private:

  bool pixelCompareImages( const QImage &a, const QImage &b );

};

测试标签

清单 16-23 显示了testTags测试槽的实现。执行的测试很简单,程序如下:

  1. 确保没有来自开始测试getIds的标签。
  2. 添加一个图像,并确保该图像在集合中—测试addImage
  3. 向图像添加一个标签,并验证集合是否包含一个标签——测试addTaggetTags
  4. 向图像中再添加一个标签,并验证该集合包含两个标签—tests addTaggetTags
  5. 向图像中再添加一个标签,并验证该集合包含三个标签—tests addTaggetTags
  6. 向图像添加一个重复的标签,并验证该集合包含三个标签—tests addTaggetTags
  7. 向一个不存在的图像添加一个新标签,并验证该集合包含三个标签—testaddTaggetTags

在清单中,您可以看到ImageCollection对象被创建,然后测试被执行。最后一个测试之前是一个QEXPECT_FAIL宏,这表明测试预计会失败,因为图像集合在添加标签之前无法检查图像 id 是否存在。

测试槽中的最后一行删除了图像收集使用的数据库连接。这是必要的,因为图像集合类依赖于默认连接。如果创建了一个新的图像收集对象(例如,在下一个测试用例中),如果原始连接没有被移除,QtSql模块将警告数据库连接正在被替换。

清单 16-23。 测试标记保持能力

void ImageCollectionTest::testTags()

{

  ImageCollection c;

  // Make sure that the collection is empty

  QCOMPARE( c.getTags().count(), 0 );

  // At least one image is needed to be able to add tags

  c.addImage( QImage( "test.png" ), QStringList() );

  // Verify that we have one image and get the id for it

  QList<int> ids = c.getIds( QStringList() );

  QCOMPARE( ids.count(), 1 );

  int id = ids[0];

  // Add one tag, total one

  c.addTag( id, "Foo" );

  QCOMPARE( c.getTags().count(), 1 );

  // Add one tag, total two

  c.addTag( id, "Bar" );

  QCOMPARE( c.getTags().count(), 2 );

  // Add one tag, total three

  c.addTag( id, "Baz" );

  QCOMPARE( c.getTags().count(), 3 );

  // Add a duplicate tag, total three

  c.addTag( id, "Foo" );

  QCOMPARE( c.getTags().count(), 3 );

  // Try to add a tag to a nonexisting id

  QEXPECT_FAIL("", "The tag will be added to the non-existing image.", Continue);

  c.addTag( id+1, "Foz" );

  QCOMPARE( c.getTags().count(), 3 );

  // The ImageConnection adds a database that we close here

  QSqlDatabase::removeDatabase( QLatin1String( QSqlDatabase::defaultConnection ) );

}

测试图像存储和检索

下一个测试用例,如清单 16-24 中的所示,检查图像存储和检索机制是否工作,并在testImages插槽中实现。

测试过程非常简单:向数据库中添加一个图像(测试addImage),确保它在那里(测试getIds),检索它(测试getImage,并将其与原始图像进行比较。

最后一个测试(已经被注释掉)试图使用无效的 id 来检索图像。这会导致调用ImageCollection类中的qFatal,即使你调用QTest::ignoreMessage(QString),应用程序也会结束。另外,ignoreMessage可以方便地避免显示使用qDebugqWarning发出的预期警告信息。

清单 16-24。 测试存储和检索图像

void ImageCollectionTest::testImages()

{

  ImageCollection c;

  QCOMPARE( c.getIds( QStringList() ).count(), 0 );

  QImage image( "test.png" );

  c.addImage( image, QStringList() );

  // Verify that we have one image and get the id for it

  QList<int> ids = c.getIds( QStringList() );

  QCOMPARE( ids.count(), 1 );

  int id = ids[0];

  QImage fromDb = c.getImage( id );

  QVERIFY( pixelCompareImages( image, fromDb ) );

// Will call qFatal and end the application

//  QTest::ignoreMessage( QtFatalMsg, "Failed to get image id" );

//  fromDb = c.getImage( id+1 );

//  QVERIFY( fromDb.isNull() );

  // The ImageConnection adds a database that we close here

  QSqlDatabase::removeDatabase( QLatin1String( QSqlDatabase::defaultConnection ) );

}

测试图像和标签

最终的测试用例testImagesFromTags,如清单 16-25 所示。这个测试一开始看起来很混乱,但是原则是检查每个给定标签返回的图像 id 的数量是否正确。为此,一次添加一个图像;然后调用getIds方法,将返回的 id 数与预期结果进行比较。整个过程描述如下:

  1. 添加带有标签FooBar的图像。
  2. 验证getTags返回两个标签。
  3. 验证返回的FooBarBaz的 id 数;以及包含FooBar的列表。
  4. 添加标签为Baz的图像。
  5. 验证getTags返回三个标签。
  6. 如果返回了FooBarBaz的 id,则验证编号。
  7. 添加带有标签BarBaz的图像。
  8. 验证getTags返回三个标签。
  9. 验证返回的FooBarBaz的 id 数;以及包含BarBaz的列表。

为了确定每组标签的预期 id 数,重要的是要记住getIds应该返回至少有一个给定标签的每个图像。这意味着当使用BarBaz查询图像时,所有三个图像 id 都会被返回。第一个图像包含Bar,第二个包含Baz,第三个包含两者。

清单 16-25。 立刻测试图像和标签

void ImageCollectionTest::testImagesFromTags()

{

  ImageCollection c;

  QCOMPARE( c.getIds( QStringList() ).count(), 0 );

  QImage image( "test.png" );

  QStringList tags;

  tags << "Foo" << "Bar";

  c.addImage( image, tags );

  QCOMPARE( c.getTags().count(), 2 );

  QCOMPARE( c.getIds( QStringList() ).count(), 1 );

  QCOMPARE( c.getIds( QStringList() << "Foo" ).count(), 1 );

  QCOMPARE( c.getIds( QStringList() << "Bar" ).count(), 1 );

  QCOMPARE( c.getIds( tags ).count(), 1 );

  QCOMPARE( c.getIds( QStringList() << "Baz" ).count(), 0 );

  tags.clear();

  tags << "Baz";

  c.addImage( image, tags );

  QCOMPARE( c.getTags().count(), 3 );

  QCOMPARE( c.getIds( QStringList() ).count(), 2 );

  QCOMPARE( c.getIds( QStringList() << "Foo" ).count(), 1 );

  QCOMPARE( c.getIds( QStringList() << "Bar" ).count(), 1 );

  QCOMPARE( c.getIds( tags ).count(), 1 );

  QCOMPARE( c.getIds( QStringList() << "Baz" ).count(), 1 );

  tags.clear();

  tags << "Bar" << "Baz";

  c.addImage( image, tags );

  QCOMPARE( c.getTags().count(), 3 );

  QCOMPARE( c.getIds( QStringList() ).count(), 3 );

  QCOMPARE( c.getIds( QStringList() << "Foo" ).count(), 1 );

  QCOMPARE( c.getIds( QStringList() << "Bar" ).count(), 2 );

  QCOMPARE( c.getIds( tags ).count(), 3 );

  QCOMPARE( c.getIds( QStringList() << "Baz" ).count(), 2 );

  // The ImageConnection adds a database that we close here

  QSqlDatabase::removeDatabase( QLatin1String( QSqlDatabase::defaultConnection ) );

}

bool ImageCollectionTest::pixelCompareImages( const QImage &a, const QImage &b )

{

  if( a.size() != b.size() )

    return false;

  if( a.format() != b.format() )

    return false;

  for( int x=0; x<a.width(); ++x )

    for( int y=0; y<a.height(); ++y )

      if( a.pixel(x,y) != b.pixel(x,y) )

        return false;

  return true;

}
处理偏差

看了测试用例之后,您可能想看看测试一个为特定应用程序设计的类的结果。我们得到的教训是事情并不完美,您必须处理测试用例中的不完美之处。

当遇到调试和警告消息时,可以通过调用QTest::ignoreMessage(QString)方法来抑制它们。很高兴知道这个方法不能用来阻止一个qFatal消息停止单元测试应用程序。

如果测试失败,您可以通过使用QEXPECT_FAIL宏来防止单元测试停止。宏在结果日志中被报告为XFAIL项,但是测试用例仍然被认为是通过的。参见清单 16-26 中的示例。

不得不在ImageCollectionTest类中进行的最令人不安的修改是避免QtSql模块警告默认连接被替换的变通方法。这个消息可以通过使用QTest::ignoreMessage方法删除。相反,通过在每个测试用例结束时移除默认连接,从单元测试中修复了该问题。这两种方法都表明,ImageCollection类仅限于在每次运行使用它的应用程序时创建一次。

清单 16-26。 测试 ImageCollection 的结果

********* Start testing of ImageCollectionTest *********

Config: Using QTest library 4.2.2, Qt 4.2.2

PASS   : ImageCollectionTest::initTestCase()

XFAIL  : ImageCollectionTest::testTags() The tag will be added to the

non-existing image.

imagecollectiontest.cpp(43) : failure location

PASS   : ImageCollectionTest::testTags()

PASS   : ImageCollectionTest::testImages()

PASS   : ImageCollectionTest::testImagesFromTags()

PASS   : ImageCollectionTest::cleanupTestCase()

Totals: 5 passed, 0 failed, 0 skipped

********* Finished testing of ImageCollectionTest *********

这里描述的每一个症状和方法都表明在被测试的类中需要调整一些东西。在测试时,有时可能不得不抑制意外的警告,但在一般情况下这是不必要的。

当考虑要测试什么时,重要的是尝试超出预期。通过测试代码对无效输入数据的反应,您可以创建更健壮的代码。通过不让您的代码进入未定义的状态,您使得应用程序的其余部分更容易调试。否则,错误的发现可能会被延迟,因为直到有缺陷的组件与应用程序的其余部分进行交互时,错误才变得可见。

总结

单元测试是一种确保您的软件组件满足规范的方法,这使得将项目中的测试资源集中在更有用的领域成为可能。

重要的是集中测试接口,而不是测试类的内部。测试不仅应该测试有效的和预期的数据;他们还应该通过传递意外数据来“挑衅”。这种“挑衅”有助于使您的软件组件更加健壮。

Qt 的单元测试框架,即QtTest模块,可以通过在项目文件中添加一行代码CONFIG += qtestlib来包含在项目中。该模块由一组用于测试的宏组成:

  • QCOMPARE( actual value, expected value ):将实际值与期望值进行比较。
  • QVERIFY( expression ):评估表达式,如果结果为true,则认为测试通过。
  • QTEST( actual value, column name ):将实际值与当前数据行的列值进行比较。

当使用QTEST宏时,您需要通过使用数据槽为您的测试提供数据的测试向量,该数据槽与测试槽同名,但以_data结束。数据槽通过使用静态的QTest::addColumn<type>(char*)方法创建一组列,然后使用静态的QTest::newRow(char*)方法添加数据行,数据是通过使用<<操作符输入的。可以用QFETCH(type, column name)宏或QTEST宏从测试槽中检索数据。

测试 Qt 组件时,能够截取信号非常重要。它们通过使用QSignalSpy类被截取和记录。

当从一个单元测试构建一个可执行文件时,使用QTEST_MAIN( test class )宏创建main函数。main函数负责创建单元测试类的实例并执行测试。***

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值