6. 单元测试

从2007年11月起,我们要求所有进入Master的新特性都必须伴随着单元测试。最初,我们将这一要求限制在QGIScore上,一旦人们熟悉了下面几节中介绍的单元测试过程,我们将把这一要求扩展到代码库的其他部分。

6.1. QGIS测试框架--概述

单元测试是使用QTestLib(Qt测试库)和CTest(作为CMake构建过程的一部分编译和运行测试的框架)的组合来执行的。在我们深入研究细节之前,让我们先概述一下这个过程:

  1. 有一些代码需要测试,例如类或函数。极限编程倡导者建议,在开始构建测试时甚至不应该编写代码,然后当您实现代码时,您可以立即验证您在测试中添加的每个新功能部分。在实践中,您可能需要为QGIS中预先存在的代码编写测试,因为我们是在许多应用程序逻辑已经实现之后很久才从测试框架开始的。

  2. 您可以创建一个单元测试。这发生在 <QGIS Source Dir>/tests/src/core 在核心库的情况下。该测试基本上是一个客户端,它创建一个类的实例并调用该类上的一些方法。它将检查每个方法的返回,以确保它与期望值匹配。如果任何一个呼叫失败,该单元就会失败。

  3. 您可以在测试类中包含QtTestLib宏。此宏由Qt元对象编译器(MOC)处理,并将测试类扩展为可运行的应用程序。

  4. 您可以在测试目录中的CMakeLists.txt中添加一个构建测试的部分。

  5. 你要确保你有 ENABLE_TESTING 在ccmake/cmakeetup中启用。这将确保您的测试在您输入make时得到实际编译。

  6. 您可以选择将测试数据添加到 <QGIS Source Dir>/tests/testdata 如果您的测试是数据驱动的(例如,需要加载shapefile)。这些测试数据应该尽可能小,并且只要有可能,您就应该使用已有的数据集。你的测试永远不应该在原地修改这些数据,而是在需要的时候在其他地方复制一个临时副本。

  7. 您编译您的源代码并安装。使用NORMAL执行此操作 make && (sudo)  make install 程序。

  8. 你来做你的测试。这通常是通过简单地通过执行 make testmake install 步骤,不过我们将解释提供对运行测试的更细粒度控制的其他方法。

考虑到这一概述,我们将深入研究一些细节。我们已经在CMake和源代码树中的其他位置为您完成了大部分配置,所以您所需要做的就是编写简单的单元测试!

6.2. 创建单元测试

创建单元测试很简单--通常只需创建一个 .cpp 文件(编号 .h 文件),并将所有测试方法实现为返回空的公共方法。我们将使用一个简单的测试类来 QgsRasterLayer 在接下来的部分中进行说明。按照惯例,我们将使用与它们正在测试的类相同的名称来命名我们的测试,但前缀为‘Test’。因此,我们的测试实现放在一个名为 testqgsrasterlayer.cpp 而班级本身也将是 TestQgsRasterLayer 。首先,我们添加标准版权横幅:

/***************************************************************************
 testqgsvectorfilewriter.cpp
 --------------------------------------
  Date : Friday, Jan 27, 2015
  Copyright: (C) 2015 by Tim Sutton
  Email: tim@kartoza.com
 ***************************************************************************
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 ***************************************************************************/

接下来,我们开始我们计划运行的测试所需的包含项。有一个特殊的包括所有测试应该具备的条件:

#include <QtTest/QtTest>

除此之外,您只需像往常一样继续实现您的类,拉入您可能需要的任何标头:

//Qt includes...
#include <QObject>
#include <QString>
#include <QObject>
#include <QApplication>
#include <QFileInfo>
#include <QDir>

//qgis includes...
#include <qgsrasterlayer.h>
#include <qgsrasterbandstats.h>
#include <qgsapplication.h>

因为我们将类声明和实现合并到一个文件中,所以接下来是类声明。我们从doxygen文档开始。每个测试用例都应该被适当地记录下来。我们使用doxygen ingroup指令,以便所有的单元测试在生成的DOOXO文档中显示为一个模块。之后是对单元测试的简短描述,类必须继承自QObject并包含Q_OBJECT宏。

/** \ingroup UnitTests
 * This is a unit test for the QgsRasterLayer class.
 */

class TestQgsRasterLayer: public QObject
{
    Q_OBJECT

我们的所有测试方法都是作为私有插槽实现的。QtTest框架将顺序调用测试类中的每个私有槽方法。有四个“特殊”方法,如果实现,将在单元测试开始时调用 (initTestCase ),在单元测试结束时 (cleanupTestCase )。在调用每个测试方法之前, init() 方法,并且在调用每个测试方法之后, cleanup() 方法被调用。这些方法很方便,因为它们允许您在运行每个测试以及整个测试单元之前分配和清理资源。

private slots:
  // will be called before the first testfunction is executed.
  void initTestCase();
  // will be called after the last testfunction was executed.
  void cleanupTestCase(){};
  // will be called before each testfunction is executed.
  void init(){};
  // will be called after every testfunction.
  void cleanup();

然后是您的测试方法,所有这些方法都不应该带参数,并且都应该返回空。这些方法将按声明顺序调用。我们在这里实现了两种方法,它们说明了两种类型的测试。

在第一种情况下,我们想要一般性地测试类的各个部分是否工作正常,我们可以使用功能测试方法。同样,极端程序员会主张在实现类之前编写这些测试。然后,在完成类实现的过程中,您可以迭代地运行单元测试。随着类实现工作的进展,应该会有越来越多的测试功能成功完成,当整个单元测试通过时,您的新类就完成了,并且现在用一种可重复的方式来验证它。

通常,您的单元测试将只覆盖您的类的公共API,通常您不需要为访问器和变更器编写测试。如果发生访问器或赋值函数未按预期工作的情况,您通常会实现 regression test 来检查这个。

//
// Functional Testing
//

/** Check if a raster is valid. */
void isValid();

// more functional tests here ...

6.2.1. 实现回归测试

接下来,我们实现回归测试。应该实施回归测试,以复制特定错误的条件。例如:

  1. 我们通过电子邮件收到了一份报告,称栅格的像元计数减少了1,丢弃了栅格波段的所有统计数据。

  2. 我们打开了一个错误报告 (ticket #832 )

  3. 我们使用一个小的测试数据集(10x10栅格)创建了一个复制错误的回归测试。

  4. 我们运行了测试,验证它确实失败了(细胞计数是99而不是100)。

  5. 然后我们修复了错误并重新运行了单元测试,回归测试通过了。我们提交了回归测试以及错误修复。现在,如果任何人在将来再次破坏源代码中的这一点,我们可以立即识别代码已经退化。

    更好的是,在未来提交任何更改之前,运行我们的测试将确保我们的更改不会有意想不到的副作用--比如破坏现有功能。

回归测试还有一个好处--它们可以节省您的时间。如果您曾经修复过一个错误,该错误涉及对源代码进行更改,然后运行应用程序并执行一系列复杂的步骤来复制该问题,那么显而易见的是,在修复错误之前简单地实现回归测试将使您能够以高效的方式自动化测试以解决错误。

要实现回归测试,您应该遵循 regression<TicketID> 为您的测试功能。如果不存在回归的票证,则应首先创建一个票证。使用这种方法,运行失败的回归测试的人员可以轻松地找到更多信息。

//
// Regression Testing
//

/** This is our second test case...to check if a raster
 *  reports its dimensions properly. It is a regression test
 *  for ticket #832 which was fixed with change r7650.
 */
void regression832();

// more regression tests go here ...

最后,在测试类声明中,您可以私下声明单元测试可能需要的任何数据成员和帮助器方法。在我们的示例中,我们将声明一个 QgsRasterLayer * 它可以被我们的任何测试方法使用。栅格图层将在中创建 initTestCase() 函数,该函数在任何其他测试之前运行,然后使用 cleanupTestCase() 它在所有测试之后运行。通过私下声明帮助器方法(可以由各种测试函数调用),您可以确保它们不会由我们在编译测试时创建的QTest可执行文件自动运行。

  private:
    // Here we have any data structures that may need to
    // be used in many test cases.
    QgsRasterLayer * mpLayer;
};

我们的类声明到此结束。实现只是内联在下面的同一文件中。首先,我们的初始化和清理函数:

void TestQgsRasterLayer::initTestCase()
{
  // init QGIS's paths - true means that all path will be inited from prefix
  QString qgisPath = QCoreApplication::applicationDirPath ();
  QgsApplication::setPrefixPath(qgisPath, TRUE);
#ifdef Q_OS_LINUX
  QgsApplication::setPkgDataPath(qgisPath + "/../share/qgis");
#endif
  //create some objects that will be used in all tests...

  std::cout << "PrefixPATH: " << QgsApplication::prefixPath().toLocal8Bit().data() << std::endl;
  std::cout << "PluginPATH: " << QgsApplication::pluginPath().toLocal8Bit().data() << std::endl;
  std::cout << "PkgData PATH: " << QgsApplication::pkgDataPath().toLocal8Bit().data() << std::endl;
  std::cout << "User DB PATH: " << QgsApplication::qgisUserDbFilePath().toLocal8Bit().data() << std::endl;

  //create a raster layer that will be used in all tests...
  QString myFileName (TEST_DATA_DIR); //defined in CmakeLists.txt
  myFileName = myFileName + QDir::separator() + "tenbytenraster.asc";
  QFileInfo myRasterFileInfo ( myFileName );
  mpLayer = new QgsRasterLayer ( myRasterFileInfo.filePath(),
  myRasterFileInfo.completeBaseName() );
}

void TestQgsRasterLayer::cleanupTestCase()
{
  delete mpLayer;
}

上面的init函数说明了一些有趣的事情。

  1. 我们需要手动设置QGIS应用程序数据路径,以便 srs.db 都能被正确地找到。

  2. 其次,这是一个数据驱动的测试,因此我们需要提供一种方法来一般性地定位 tenbytenraster.asc 文件。这是通过使用编译器定义 TEST_DATA_PATH 。定义是在 CMakeLists.txt 下的配置文件 <QGIS Source Root>/tests/CMakeLists.txt 并且可用于所有QGIS单元测试。如果您的测试需要测试数据,请在 <QGIS Source Root>/tests/testdata 。在这里,您应该只提交非常小的数据集。如果您的测试需要修改测试数据,它应该首先对其进行复制。

Qt还为数据驱动测试提供了一些其他有趣的机制,因此,如果您有兴趣了解更多有关该主题的信息,请参考Qt文档。

接下来,让我们来看看我们的功能测试。这个 isValid() 测试只是检查栅格层是否已正确加载到initTestCase中。QVERIFY是可用于评估测试条件的Qt宏。您的测试中还提供了其他一些使用Qt的用法宏,包括:

  • QCOMPARE( actual, expected )

  • QEXPECT_FAIL( dataIndex, comment, mode )

  • QFAIL( message )

  • QFETCH( type, name )

  • QSKIP( description, mode )

  • QTEST( actual, testElement )

  • QTEST_APPLESS_Main( TestClass )

  • QTEST_Main( TestClass )

  • QTEST_NOOP_Main()

  • QVERIFY2( condition, message )

  • QVERIFY( condition )

  • QWARN( message )

其中一些宏只有在使用Qt框架进行数据驱动测试时才有用(有关更多详细信息,请参阅Qt文档)。

void TestQgsRasterLayer::isValid()
{
  QVERIFY ( mpLayer->isValid() );
}

通常,在可行的情况下,您的功能测试将覆盖您的类公共API的所有功能范围。有了功能测试,我们就可以查看回归测试示例了。

由于Bug#832中的问题是错误报告的细胞计数,因此编写我们的测试只需使用QVERIFY来检查细胞计数是否符合预期值:

void TestQgsRasterLayer::regression832()
{
  QVERIFY ( mpLayer->getRasterXDim() == 10 );
  QVERIFY ( mpLayer->getRasterYDim() == 10 );
  // regression check for ticket #832
  // note getRasterBandStats call is base 1
  QVERIFY ( mpLayer->getRasterBandStats(1).elementCountInt == 100 );
}

实现了所有单元测试函数后,我们还需要向测试类添加最后一件事:

QTEST_MAIN(TestQgsRasterLayer)
#include "testqgsrasterlayer.moc"

这两行的目的是通知Qt的moc这是一个QtTest(它将生成一个main方法,该方法依次调用每个测试函数)。最后一行是包含MOC生成的源代码。你应该换掉 testqgsrasterlayer 用小写字母表示您所在的班名。

6.3. 比较图像以进行渲染测试

由于平台特定的实现(例如,不同的字体呈现和抗锯齿算法)、系统上可用的字体以及其他模糊的原因,在不同环境上呈现图像可能会产生细微的差异。

当在Travis上运行渲染测试但失败时,请在Travis日志的最底部查找破折号链接。这个链接将把你带到一个cdash页面,在那里你可以看到渲染的图像与预期的图像,以及一个用红色突出显示与参考图像不匹配的像素的“不同”图像。

QGIS单元测试系统支持添加“掩码”图像,用于指示渲染图像何时可能与参考图像不同。遮罩图像是一种图像(与参考图像同名,但包括 _mask.png 后缀),并且应该与参考图像具有相同的尺寸。在遮罩图像中,像素值指示该单个像素与参考图像的差异程度,因此黑色像素表示渲染图像中的像素必须与参考图像中的相同像素完全匹配。具有RGB 2、2、2的像素意味着渲染图像的RGB值可能与参考图像相差最多2,而全白像素(255、255、255)意味着在比较预期图像和渲染图像时,该像素实际上被忽略。

下面提供了用于生成遮罩图像的实用程序脚本 scripts/generate_test_mask_image.py 。此脚本通过向其传递参考图像的路径(例如 tests/testdata/control_images/annotations/expected_annotation_fillstyle/expected_annotation_fillstyle.png )和渲染图像的路径。

例如。

scripts/generate_test_mask_image.py tests/testdata/control_images/annotations/expected_annotation_fillstyle/expected_annotation_fillstyle.png /tmp/path_to_rendered_image.png

您可以通过传递测试名称的一部分来缩短指向参考图像的路径,例如

scripts/generate_test_mask_image.py annotation_fillstyle /tmp/path_to_rendered_image.png

(此快捷方式仅在找到单个匹配的参考图像时才起作用。如果找到多个匹配项,则需要提供参考图像的完整路径。)

该脚本还接受渲染图像的http URL,因此您可以直接从cdash结果页面复制渲染图像URL并将其传递给脚本。

生成蒙版图像时要小心-您应该始终查看生成的蒙版图像并检查图像中的任何白色区域。由于这些像素被忽略,请确保这些白色图像不会覆盖参考图像的任何重要部分--否则您的单元测试将毫无意义!

同样,如果您有意要将遮罩的某些部分从测试中排除,您可以手动将它们“涂白”。这对于混合了符号和文本呈现的测试(例如图例测试)很有用,其中单元测试不是为测试呈现的文本而设计的,并且您不希望测试受到跨平台文本呈现差异的影响。

要比较QGIS单元测试中的图像,您应该使用类 QgsMultiRenderChecker 或者它的一个子类。

为了提高测试的健壮性,这里有几个小贴士:

  1. 如果可以,请禁用抗锯齿,因为这样可以最大限度地减少跨平台渲染差异。

  2. 确保你的参考图片是“厚实的”。也就是说,不要有1像素的宽线或其他精细的特征,并使用大的粗体字体(推荐14磅或更多)。

  3. 有时测试会生成大小略有不同的图像(例如图例渲染测试,其中图像大小取决于字体渲染大小--这会受到跨平台差异的影响)。要说明这一点,请使用 QgsMultiRenderChecker::setSizeTolerance() 并指定渲染图像的宽度和高度与参考图像不同的最大像素数。

  4. 不要在参考图像中使用透明背景(CDash不支持它们)。相反,您可以使用 QgsMultiRenderChecker::drawBackground() 要绘制参考图像背景的棋盘图案,请执行以下操作。

  5. 当需要字体时,请使用 QgsFontUtils::standardTestFontFamily() (“QGIS Vera Sans”)。

如果Travis报告新图像的错误(例如,由于抗锯齿或字体差异),则脚本 parse_dash_results.py 可以在更新本地测试掩码时为您提供帮助。

6.4. 将单元测试添加到CMakeLists.txt

将您的单元测试添加到构建系统只需编辑 CMakeLists.txt 在测试目录中,克隆一个现有的测试块,然后将您的测试类名替换到其中。例如:

# QgsRasterLayer test
ADD_QGIS_TEST(rasterlayertest testqgsrasterlayer.cpp)

6.4.1. 解释了ADD_QGIS_TEST宏

我们将简要介绍这些代码行来解释它们的作用,但如果您不感兴趣,只需执行上面部分中解释的步骤即可。

MACRO (ADD_QGIS_TEST testname testsrc)
SET(qgis_${testname}_SRCS ${testsrc} ${util_SRCS})
SET(qgis_${testname}_MOC_CPPS ${testsrc})
QT4_WRAP_CPP(qgis_${testname}_MOC_SRCS ${qgis_${testname}_MOC_CPPS})
ADD_CUSTOM_TARGET(qgis_${testname}moc ALL DEPENDS ${qgis_${testname}_MOC_SRCS})
ADD_EXECUTABLE(qgis_${testname} ${qgis_${testname}_SRCS})
ADD_DEPENDENCIES(qgis_${testname} qgis_${testname}moc)
TARGET_LINK_LIBRARIES(qgis_${testname} ${QT_LIBRARIES} qgis_core)
SET_TARGET_PROPERTIES(qgis_${testname}
PROPERTIES
# skip the full RPATH for the build tree
SKIP_BUILD_RPATHTRUE
# when building, use the install RPATH already
# (so it doesn't need to relink when installing)
BUILD_WITH_INSTALL_RPATH TRUE
# the RPATH to be used when installing
INSTALL_RPATH ${QGIS_LIB_DIR}
# add the automatically determined parts of the RPATH
# which point to directories outside the build tree to the install RPATH
INSTALL_RPATH_USE_LINK_PATH true)
IF (APPLE)
# For macOS, the executable must be at the root of the bundle's executable folder
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX})
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/qgis_${testname})
ELSE (APPLE)
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/bin/qgis_${testname})
ENDIF (APPLE)
ENDMACRO (ADD_QGIS_TEST)

让我们更详细地看看每一行。首先,我们定义测试的来源列表。因为我们只有一个源文件(遵循上面描述的方法,类声明和定义在同一个文件中),所以它是一个简单的语句:

SET(qgis_${testname}_SRCS ${testsrc} ${util_SRCS})

由于我们的测试类需要通过Qt元对象编译器(MOC)运行,因此我们也需要提供几行代码来实现这一点:

SET(qgis_${testname}_MOC_CPPS ${testsrc})
QT4_WRAP_CPP(qgis_${testname}_MOC_SRCS ${qgis_${testname}_MOC_CPPS})
ADD_CUSTOM_TARGET(qgis_${testname}moc ALL DEPENDS ${qgis_${testname}_MOC_SRCS})

接下来,我们告诉cmake它必须从测试类生成一个可执行文件。请记住,在上一节关于类实现的最后一行中,我们将moc输出直接包含到我们的测试类中,因此这将为它提供一个main方法,以便可以将类编译为可执行文件:

ADD_EXECUTABLE(qgis_${testname} ${qgis_${testname}_SRCS})
ADD_DEPENDENCIES(qgis_${testname} qgis_${testname}moc)

接下来,我们需要指定任何库依赖项。目前,类已经实现了一个通用的Qt_LIBRARY依赖关系,但我们将努力用每个类只需要的特定Qt库来替换它。当然,您还需要按照单元测试的要求链接到相关的QGIS库。

TARGET_LINK_LIBRARIES(qgis_${testname} ${QT_LIBRARIES} qgis_core)

接下来,我们告诉cmake将测试安装到与QGIS二进制文件本身相同的位置。这是我们计划在将来删除的东西,这样测试就可以直接从源代码树内部运行。

SET_TARGET_PROPERTIES(qgis_${testname}
PROPERTIES
# skip the full RPATH for the build tree
SKIP_BUILD_RPATHTRUE
# when building, use the install RPATH already
# (so it doesn't need to relink when installing)
BUILD_WITH_INSTALL_RPATH TRUE
# the RPATH to be used when installing
INSTALL_RPATH ${QGIS_LIB_DIR}
# add the automatically determined parts of the RPATH
# which point to directories outside the build tree to the install RPATH
INSTALL_RPATH_USE_LINK_PATH true)
IF (APPLE)
# For macOS, the executable must be at the root of the bundle's executable folder
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX})
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/qgis_${testname})
ELSE (APPLE)
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/bin/qgis_${testname})
ENDIF (APPLE)

最后,上面的用法 ADD_TEST 将测试注册到cmake/ctest。这里是最神奇的地方--我们用ctest注册这个类。如果您还记得我们在本节开头给出的概述,我们同时使用QtTest和CTest。简单地说,QtTest向您的测试单元添加了一个Main方法,并在类中处理对测试方法的调用。它还提供了一些宏,如 QVERIFY 您可以使用条件来测试测试是否失败。QtTest单元测试的输出是一个可执行文件,您可以从命令行运行它。但是,当您有一套测试,并且希望依次运行每个可执行文件,并且更好地将运行测试集成到构建过程中时,我们将使用CTest。

6.5. 构建您的单元测试

要构建单元测试,您只需确保 ENABLE_TESTS=true 在cmake配置中。有两种方法可以做到这一点:

  1. ccmake .. (或 cmakesetup .. 在窗口中),并交互地设置 ENABLE_TESTS 标志为 ON

  2. 将命令行标志添加到cmake,例如 cmake -DENABLE_TESTS=true ..

除此之外,只需按照正常方式构建QGIS,测试也应该构建。

6.6. 运行您的测试

运行测试的最简单方法是将其作为正常构建过程的一部分:

make && make install && make test

这个 make test 命令将调用CTest,CTest将运行使用上述ADD_TEST CMake指令注册的每个测试。典型输出来自 make test 将如下所示:

Running tests...
Start processing tests
Test project /Users/tim/dev/cpp/qgis/build
## 13 Testing qgis_applicationtest***Exception: Other
## 23 Testing qgis_filewritertest *** Passed
## 33 Testing qgis_rasterlayertest*** Passed

## 0 tests passed, 3 tests failed out of 3

The following tests FAILED:
## 1- qgis_applicationtest (OTHER_FAULT)
Errors while running CTest
make: *** [test] Error 8

如果测试失败,您可以使用ctest命令更仔细地检查失败的原因。使用 -R 选项,以指定要为其运行测试的正则表达式和 -V 要获取详细输出,请执行以下操作:

$ ctest -R appl -V

Start processing tests
Test project /Users/tim/dev/cpp/qgis/build
Constructing a list of tests
Done constructing a list of tests
Changing directory into /Users/tim/dev/cpp/qgis/build/tests/src/core
## 13 Testing qgis_applicationtest
Test command: /Users/tim/dev/cpp/qgis/build/tests/src/core/qgis_applicationtest
********* Start testing of TestQgsApplication *********
Config: Using QTest library 4.3.0, Qt 4.3.0
PASS : TestQgsApplication::initTestCase()
PrefixPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
PluginPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//lib/qgis
PkgData PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis
User DB PATH: /Users/tim/.qgis/qgis.db
PASS : TestQgsApplication::getPaths()
PrefixPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
PluginPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//lib/qgis
PkgData PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis
User DB PATH: /Users/tim/.qgis/qgis.db
QDEBUG : TestQgsApplication::checkTheme() Checking if a theme icon exists:
QDEBUG : TestQgsApplication::checkTheme()
/Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis/themes/default//mIconProjectionDisabled.png
FAIL!: TestQgsApplication::checkTheme() '!myPixmap.isNull()' returned FALSE. ()
Loc: [/Users/tim/dev/cpp/qgis/tests/src/core/testqgsapplication.cpp(59)]
PASS : TestQgsApplication::cleanupTestCase()
Totals: 3 passed, 1 failed, 0 skipped
********* Finished testing of TestQgsApplication *********
-- Process completed
***Failed

## 0 tests passed, 1 tests failed out of 1

The following tests FAILED:
## 1- qgis_applicationtest (Failed)
Errors while running CTest

6.6.1. 运行单独的测试

C++测试是普通的应用程序。您可以像运行任何可执行文件一样,从Build文件夹中运行它们。

$ ./output/bin/qgis_dxfexporttest

********* Start testing of TestQgsDxfExport *********
Config: Using QtTest library 5.12.5, Qt 5.12.5 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 9.2.1 20190827 (Red Hat 9.2.1-1))
PASS   : TestQgsDxfExport::initTestCase()
PASS   : TestQgsDxfExport::testPoints()
PASS   : TestQgsDxfExport::testLines()
...
Totals: 19 passed, 4 failed, 0 skipped, 0 blacklisted, 612ms
********* Finished testing of TestQgsDxfExport *********

这些测试还需要 command line arguments 。这使得可以运行特定的测试子集:

$ ./output/bin/qgis_dxfexporttest testPoints
********* Start testing of TestQgsDxfExport *********
Config: Using QtTest library 5.12.5, Qt 5.12.5 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 9.2.1 20190827 (Red Hat 9.2.1-1))
PASS   : TestQgsDxfExport::initTestCase()
PASS   : TestQgsDxfExport::testPoints()
PASS   : TestQgsDxfExport::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 272ms
********* Finished testing of TestQgsDxfExport *********

6.6.2. 调试单元测试

C++测试

对于C++单元测试,QtCreator会自动添加运行目标,因此您可以从调试器启动它们。

如果您转到 Projects 在那里,到了 Build & Run -->台式机 Run 选项卡中,还可以指定允许在调试器的.cpp文件中运行测试子集的命令行参数。

Python 测试

还可以使用GDB从QtCreator启动Python单元测试。为此,您需要转到 Projects 并选择 Run 在……下面 Build & Run 。然后添加一个新的 Run configuration 使用可执行文件 /usr/bin/python3 并将命令行参数设置为单元测试python文件的路径,例如 /home/user/dev/qgis/QGIS/tests/src/python/test_qgsattributeformeditorwidget.py

现在还更改了 Run Environment 并增加3个新变量:

变量

价值

PYTHONPATH

[build] /OUTPUT/python/: [build] /OUTPUT/python/plugins: [source] /TESTS/src/python

QGIS_PREFIX_PATH

[build] /OUTPUT

LD_LIBRARY_PATH

[build] /OUTPUT/库

替换 [build] 使用您的构建目录和 [source] 使用您的源目录。

6.6.3. 玩得开心

关于在QGIS中编写单元测试的这一节到此结束。我们希望您养成编写测试以测试新功能和检查回归的习惯。测试系统的某些方面(尤其是 CMakeLists.txt 部件),以便测试框架以一种真正独立于平台的方式工作。