当前位置: 快讯 >
使用 GoogleTest 和 CTest 进行单元测试
2023-08-02 14:31:41    来源 : Linux中国

进行单元测试可以提高代码质量,并且它不会打断你的工作流。


(资料图)

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

在上一篇文章中我介绍了基于VSCodium和CMake配置构建系统。本文我将介绍如何通过GoogleTest和CTest将单元测试集成到这个构建系统中。

首先克隆这个仓库,用 VSCodium 打开,切换到devops_2标签。你可以通过点击main分支符号(红框处),然后选择devops_2标签(黄框处)来进行切换:

VSCodium tag

或者你可以通过命令行来切换:

$ git checkout tags/devops_2
GoogleTest

GoogleTest 是一个平台无关的开源 C++ 测试框架。单元测试是用来验证单个逻辑单元的行为的。尽管 GoogleTest 并不是专门用于单元测试的,我将用它对Generator库进行单元测试。

在 GoogleTest 中,测试用例是通过断言宏来定义的。断言可能产生以下结果:

成功: 测试通过。非致命失败: 测试失败,但测试继续。致命失败: 测试失败,且测试终止。

致命断言和非致命断言通过不同的宏来区分:

ASSERT_*: 致命断言,失败时终止。EXPECT_*: 非致命断言,失败时不终止。

谷歌推荐使用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来获取对象的地址,并用EXPECT_EQ来比较输入对象和返回对象是否是同一个。

检查元素个数

本测试检查作为输入的std::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(...)函数应该生成一个 1 到 n 的乱序的序列,这个序列的和应当是不变的。

TEST(GeneratorTest, CheckSum){    const int NumberOfElements = 50;    int nChecksum_in = 0;    int nChecksum_out = 0;    std::vector 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 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文件中。下面我详细介绍 3 个测试用例:

参数正常

如果输入参数是一个正整数,程序应该输出应该是一个数列:

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")
参数错误

当传入的参数不是整数时,程序应该退出并报错。比如给Producer传入参数ABC

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

可以使用ctest -R Usage -VV命令来执行测试。这里给ctest的命令行参数:

-R <测试名称>: 执行单个测试-VV:打印详细输出

测试执行结果如下:

$ ctest -R Usage -VVUpdatecTest Configuration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tclUpdateCTestConfiguration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tclTest project /home/stephan/Documents/cpp_testing sample/buildConstructing a list of testsDone constructing a list of testsUpdating test list for fixturesAdded 0 tests to meet fixture requirementsChecking test dependency graph...Checking test dependency graph end

在这里我执行了名为Usage的测试。

它以无参数的方式调用Producer

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

输出不匹配[^[0-9]+]的正则模式,测试未通过。

3: Enter the number of elements as argument1/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 1Total Test time (real) =0.00 secThe following tests FAILED:3 - Usage (Failed)Errors while running CTest$

如果想要执行所有测试(包括那些用 GoogleTest 生成的),切换到build目录中,然后运行ctest即可:

CTest run

在 VSCodium 中可以通过点击信息栏的黄框处来调用 CTest。如果所有测试都通过了,你会看到如下输出:

VSCodium

使用 Git 钩子进行自动化测试

目前为止,运行测试是开发者需要额外执行的步骤,那些不能通过测试的代码仍然可能被提交和推送到代码仓库中。利用Git 钩子可以自动执行测试,从而防止有瑕疵的代码被提交。

切换到.git/hooks目录,创建pre-commit文件,复制粘贴下面的代码:

#!/usr/bin/sh(cd build; ctest --output-on-failure -j6)

然后,给文件增加可执行权限:

$ chmod +x pre-commit

这个脚本会在提交之前调用 CTest 进行测试。如果有测试未通过,提交过程就会被终止:

Commit failed

只有所有测试都通过了,提交过程才会完成:

Commit succeeded

这个机制也有一个漏洞:可以通过git commit --no-verify命令绕过测试。解决办法是配置构建服务器,这能保证只有正常工作的代码才能被提交,但这又是另一个话题了。

总结

本文提到的技术实施简单,并且能够帮你快速发现代码中的问题。做单元测试可以提高代码质量,同时也不会打断你的工作流。GoogleTest 框架提供了丰富的特性以应对各种测试场景,文中我所提到的只是一小部分而已。如果你想进一步了解 GoogleTest,我推荐你阅读GoogleTest Primer。

X 关闭

相关文章

Copyright ©  2015-2023 京津冀变频网版权所有  备案号:京ICP备2022022245号-12   联系邮箱:434 922 62 @qq.com