进行单元测试可以提高代码质量,并且它不会打断你的工作流。
本文是 使用 CMake 和 VSCodium 设置一个构建系统 的后续文章。
在上一篇文章中我介绍了基于 VSCodium 和 CMake 配置构建系统。本文我将介绍如何通过 GoogleTest 和 CTest 将单元测试集成到这个构建系统中。
首先克隆 这个仓库 ,用 VSCodium 打开,切换到 devops_2
标签。你可以通过点击 main
分支符号(红框处),然后选择 devops_2
标签(黄框处)来进行切换:
或者你可以通过命令行来切换:
1 2 $ git checkout tags/devops_2
GoogleTest GoogleTest 是一个平台无关的开源 C++ 测试框架。单元测试是用来验证单个逻辑单元的行为的。尽管 GoogleTest 并不是专门用于单元测试的,我将用它对 Generator
库进行单元测试。
在 GoogleTest 中,测试用例是通过断言宏来定义的。断言可能产生以下结果:
成功 : 测试通过。
非致命失败 : 测试失败,但测试继续。
致命失败 : 测试失败,且测试终止。
致命断言和非致命断言通过不同的宏来区分:
ASSERT_*
: 致命断言,失败时终止。
EXPECT_*
: 非致命断言,失败时不终止。
谷歌推荐使用 EXPECT_*
宏,因为当测试中包含多个的断言时,它允许继续执行。断言有两个参数:第一个参数是测试分组的名称,第二个参数是测试自己的名称。Generator
只定义了 generate(...)
函数,所以本文中所有的测试都属于同一个测试组:GeneratorTest
。
针对 generate(...)
函数的测试可以从 GeneratorTest.cpp 中找到。
引用一致性检查 generate(…) 函数有一个 std::stringstream 的引用作为输入参数,并且它也将这个引用作为返回值。第一个测试就是检查输入的引用和返回的引用是否一致。
1 2 3 4 5 6 7 8 9 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
引用中的元素个数与输入参数中指定的个数是否相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
函数,应该得到的是两个不同的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 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 的乱序的序列,这个序列的和应当是不变的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 TEST (GeneratorTest, CheckSum){ const int NumberOfElements = 50 ; int nChecksum_in = 0 ; int nChecksum_out = 0 ; std::vector<int > vNumbersRef (NumberOfElements) ; std::iota (vNumbersRef.begin (), vNumbersRef.end (), 1 ); for (const int n : vNumbersRef){ nChecksum_in += n; } std::stringstream buffer; Generator::generate (buffer, NumberOfElements); std::vector<int > vNumbersGen; std::string s_no; while (std::getline (buffer, s_no, ' ' )) { vNumbersGen.push_back (std::stoi (s_no)); } for (const int n : vNumbersGen){ nChecksum_out += n; } EXPECT_EQ (nChecksum_in, nChecksum_out); }
你可以像对一般 C++ 程序一样调试这些测试。
CTest 除了嵌入到代码中的测试之外,CTest 提供了可执行程序的测试方式。简而言之就是通过给可执行程序传入特定的参数,然后用 正则表达式 对它的输出进行匹配检查。通过这种方式可以很容易检查程序对于不正确的命令行参数的反应。这些测试定义在顶层的 CMakeLists.txt 文件中。下面我详细介绍 3 个测试用例:
参数正常 如果输入参数是一个正整数,程序应该输出应该是一个数列:
1 2 3 4 5 add_test (NAME RegularUsage COMMAND Producer 10 )set_tests_properties (RegularUsage PROPERTIES PASS_REGULAR_EXPRESSION "^[0-9 ]+" )
没有提供参数 如果没有传入参数,程序应该立即退出并提示错误原因:
1 2 3 4 5 add_test (NAME NoArg COMMAND Producer )set_tests_properties (NoArg PROPERTIES PASS_REGULAR_EXPRESSION "^Enter the number of elements as argument" )
参数错误 当传入的参数不是整数时,程序应该退出并报错。比如给 Producer
传入参数 ABC
:
1 2 3 4 5 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
:打印详细输出
测试执行结果如下:
1 2 3 4 5 6 7 8 9 10 11 $ ctest -R Usage -VV UpdatecTest 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
:
1 2 3 4 test 3 Start 3: Usage 3: Test command: /home/stephan/Documents/cpp testing sample/build/Producer
输出不匹配 [^[0-9]+]
的正则模式,测试未通过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 3 : Enter the number of elements as argument1 /1 test 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
即可:
在 VSCodium 中可以通过点击信息栏的黄框处来调用 CTest。如果所有测试都通过了,你会看到如下输出:
使用 Git 钩子进行自动化测试 目前为止,运行测试是开发者需要额外执行的步骤,那些不能通过测试的代码仍然可能被提交和推送到代码仓库中。利用 Git 钩子 可以自动执行测试,从而防止有瑕疵的代码被提交。
切换到 .git/hooks
目录,创建 pre-commit
文件,复制粘贴下面的代码:
1 2 3 4 #!/usr/bin/sh (cd build; ctest --output-on-failure -j6)
然后,给文件增加可执行权限:
这个脚本会在提交之前调用 CTest 进行测试。如果有测试未通过,提交过程就会被终止:
只有所有测试都通过了,提交过程才会完成:
这个机制也有一个漏洞:可以通过 git commit --no-verify
命令绕过测试。解决办法是配置构建服务器,这能保证只有正常工作的代码才能被提交,但这又是另一个话题了。
总结 本文提到的技术实施简单,并且能够帮你快速发现代码中的问题。做单元测试可以提高代码质量,同时也不会打断你的工作流。GoogleTest 框架提供了丰富的特性以应对各种测试场景,文中我所提到的只是一小部分而已。如果你想进一步了解 GoogleTest,我推荐你阅读 GoogleTest Primer 。
(题图:MJ/f212ce43-b60b-4005-b70d-8384f2ba5860)
via: https://opensource.com/article/22/1/unit-testing-googletest-ctest
作者:Stephan Avenwedde 选题:lujun9972 译者:toknow-gh 校对:wxy
本文由 LCTT 原创编译,Linux中国 荣誉推出