使用 GoogleTest 和 CTest 执行单元测试

使用单元测试可能会提高您的代码质量,并且在不干扰您工作流程的情况下实现这一目标。
27 位读者喜欢这篇文章。
Team checklist and to dos

本文是我上一篇文章 使用 CMake 和 VSCodium 设置构建系统 的后续文章。

在上一篇文章中,我展示了如何配置一个基于 VSCodiumCMake 的构建系统。本文通过使用 GoogleTestCTest 集成有意义的单元测试来改进此设置。

如果尚未完成,请克隆 存储库,在 VSCodium 中打开它,并通过单击 main 分支符号(红色标记)并选择分支(黄色标记)来检出标签 devops_2

VSCodium tag

Stephan Avenwedde (CC BY-SA 4.0)

或者,打开命令行并键入

$ git checkout tags/devops_2

GoogleTest

GoogleTest 是一个平台独立的开源 C++ 测试框架。尽管 GoogleTest 并非专门用于单元测试,但我将使用它为 Generator 库定义单元测试。通常,单元测试应验证单个逻辑单元的行为。Generator 库是一个单元,因此我将编写一些有意义的测试以确保其正常运行。

使用 GoogleTest,测试用例由断言宏定义。处理断言会生成以下结果之一

  • 成功:测试通过。
  • 非致命性失败:测试失败,但测试函数将继续执行。
  • 致命性失败:测试失败,测试函数将被中止。

断言宏遵循此方案来区分致命性失败和非致命性失败

  • ASSERT_* 致命性失败,函数被中止。
  • EXPECT_* 非致命性失败,函数不会被中止。

Google 建议使用 EXPECT_* 宏,因为当测试定义多个断言时,它们允许测试继续执行。断言宏接受两个参数:第一个参数是测试组的名称(一个可自由选择的字符串),第二个参数是测试本身的名称。Generator 库仅定义了函数 generate(...),因此本文中的测试属于同一组:GeneratorTest

以下 generate(...) 函数的单元测试可以在 GeneratorTest.cpp 中找到。

引用检查

generate(...) 函数接受对 std::stringstream 的引用作为参数,并返回相同的引用。因此,第一个测试是检查传递的引用是否与函数返回的引用相同。

TEST(GeneratorTest, ReferenceCheck){
    const int NumberOfElements = 10;
    std::stringstream buffer;
    EXPECT_EQ(
        std::addressof(buffer),
        std::addressof(Generator::generate(buffer, NumberOfElements))
    );
}

在这里,我使用 std::addressof 来检查返回对象的地址是否指向我作为输入提供的同一对象。

元素数量

此测试检查 stringstream 引用中的元素数量是否与作为参数给定的数量匹配。

TEST(GeneratorTest, NumberOfElements){
    const int NumberOfElements = 50;
    int nCalcNoElements = 0;

    std::stringstream buffer;

    Generator::generate(buffer, NumberOfElements);
    std::string s_no;

    while(std::getline(buffer, s_no, ' ')) {
        nCalcNoElements++;
    }

    EXPECT_EQ(nCalcNoElements, NumberOfElements);
}

随机打乱

此测试检查随机引擎是否正常工作。如果我连续两次调用 generate 函数,我希望不会得到相同的结果。

TEST(GeneratorTest, Shuffle){

    const int NumberOfElements = 50;

    std::stringstream buffer_A;
    std::stringstream buffer_B;

    Generator::generate(buffer_A, NumberOfElements);
    Generator::generate(buffer_B, NumberOfElements);

    EXPECT_NE(buffer_A.str(), buffer_B.str());
}

校验和

这是最大的测试。它检查从 1 到 n 的数字序列的数字之和是否与打乱后的输出序列之和相同。我期望总和匹配,因为 generate(...) 函数应该只是创建此类序列的打乱变体。

TEST(GeneratorTest, CheckSum){

    const int NumberOfElements = 50;
    int nChecksum_in = 0;
    int nChecksum_out = 0;


    std::vector<int> vNumbersRef(NumberOfElements); // Input vector
    std::iota(vNumbersRef.begin(), vNumbersRef.end(), 1); // Populate vector 

    // Calculate reference checksum
    for(const int n : vNumbersRef){
        nChecksum_in += n;
    }

    std::stringstream buffer;
    Generator::generate(buffer, NumberOfElements);

    std::vector<int> vNumbersGen; // Output vector
    std::string s_no;

    // Read the buffer back back to the output vector
    while(std::getline(buffer, s_no, ' ')) {
        vNumbersGen.push_back(std::stoi(s_no));
    }

    // Calculate output checksum
    for(const int n : vNumbersGen){
        nChecksum_out += n;
    }

    EXPECT_EQ(nChecksum_in, nChecksum_out);
}

上述测试也可以像普通的 C++ 应用程序一样进行调试。

CTest

除了代码内单元测试之外,CTest 实用程序还允许我定义可以对可执行文件执行的测试。简而言之,我使用某些参数调用可执行文件,并将输出与正则表达式匹配。这使我可以简单地检查可执行文件在命令行参数不正确时的行为。测试在顶级 CMakeLists.txt 中定义。以下是三个测试用例的详细介绍

常规用法

如果提供正整数作为命令行参数,我希望可执行文件生成一系列以空格分隔的数字

add_test(NAME RegularUsage COMMAND Producer 10)
set_tests_properties(RegularUsage
    PROPERTIES PASS_REGULAR_EXPRESSION "^[0-9 ]+"
)

无参数

如果未提供参数,程序应立即退出并显示原因

add_test(NAME NoArg COMMAND Producer)
set_tests_properties(NoArg
    PROPERTIES PASS_REGULAR_EXPRESSION "^Enter the number of elements as argument"
)

错误参数

提供无法转换为整数的参数也应导致立即退出并显示错误消息。此测试使用命令行参数"ABC" 调用 Producer 可执行文件

add_test(NAME WrongArg COMMAND Producer ABC)
set_tests_properties(WrongArg
    PROPERTIES PASS_REGULAR_EXPRESSION "^Error: Cannot parse"
)

测试测试

要运行单个测试并查看其处理方式,请从命令行调用 ctest 并提供以下参数

  • 运行单个测试:-R <test-name>
  • 启用详细输出:-VV

以下是命令 ctest -R Usage -VV:

$ ctest -R Usage -VV
UpdatecTest Configuration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl
UpdateCTestConfiguration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl
Test project /home/stephan/Documents/cpp_testing sample/build
Constructing a list of tests
Done constructing a list of tests
Updating test list for fixtures
Added 0 tests to meet fixture requirements
Checking test dependency graph... 
Checking test dependency graph end

在此代码块中,我调用了一个名为 Usage 的测试。

这在没有命令行参数的情况下运行了可执行文件

test 3
    Start 3: Usage
3: Test command: /home/stephan/Documents/cpp testing sample/build/Producer

测试失败,因为输出与正则表达式 [^[0-9]+] 不匹配。

3: Enter the number of elements as argument
1/1 test #3. Usage ................

Failed Required regular expression not found.
Regex=[^[0-9]+] 

0.00 sec round.

0% tests passed, 1 tests failed out of 1
Total Test time (real) =
0.00 sec
The following tests FAILED:
3 - Usage (Failed)
Errors while running CTest
$ 

要运行所有测试(包括使用 GoogleTest 定义的测试),请导航到 build 目录并运行 ctest

CTest run

Stephan Avenwedde (CC BY-SA 4.0)

在 VSCodium 内部,单击信息栏中标记为黄色的区域以调用 CTest。如果所有测试都通过,则会显示以下输出

VSCodium

Stephan Avenwedde (CC BY-SA 4.0)

使用 Git Hooks 自动化测试

到目前为止,运行测试对于开发人员来说是一个额外的步骤。开发人员也可能提交和推送未通过测试的代码。感谢 Git Hooks,我可以实现一种机制,该机制可以自动运行测试并防止开发人员意外提交错误代码。

导航到 .git/hooks,创建一个名为 pre-commit 的空文件,并复制粘贴以下代码

#!/usr/bin/sh

(cd build; ctest --output-on-failure -j6)

之后,使此文件可执行

$ chmod +x pre-commit

此脚本在尝试执行提交时调用 CTest。如果测试失败(如下面的屏幕截图所示),则提交将被中止

Commit failed

Stephan Avenwedde (CC BY-SA 4.0)

如果测试成功,则会处理提交,并且输出如下所示

Commit succeeded

Stephan Avenwedde (CC BY-SA 4.0)

所描述的机制只是一个软性障碍:开发人员仍然可以使用 git commit --no-verify 提交错误代码。我可以确保只推送正常工作的代码,方法是配置构建服务器。此主题将是另一篇文章的一部分。

总结

本文中提到的技术易于实施,并可帮助您快速找到代码中的错误。使用单元测试可能会提高您的代码质量,并且正如我已经展示的那样,这样做不会干扰您的工作流程。GoogleTest 框架为每种可能的场景都提供了功能;我只使用了其功能的一个子集。此时,我还想提及 GoogleTest Primer,它使您可以大致了解该框架的想法、机会和功能。

接下来阅读什么
标签
User profile image.
Stephan 是一位技术爱好者,他欣赏开源技术,以便深入了解事物的工作原理。Stephan 在工业自动化软件这个大部分是专有领域的公司担任全职支持工程师。如果可能,他会从事基于 Python 的开源项目、撰写文章或驾驶摩托车。

评论已关闭。

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.