cmake之旅10cmake之旅10自动化测试与 CTest1 CTest 基础1.1 最简单的测试1.2 CTest 常用参数1.3 add_test 的更多用法2 集成 Google Test2.1 引入 Google Test2.2 编写测试2.3 构建并运行3 用 option 控制是否构建测试4 BUILD_TESTING 惯例5 Google Test 常用断言6 本篇命令速查表7 总结与下一篇预告同系列文章cmake之旅(1):构建的过程cmake之旅(2):CMakeLists.txt 核心语法cmake之旅(3):多目录项目管理cmake之旅(4):静态库与动态库cmake之旅5):函数、宏与 .cmake 模块cmake之旅6查找和使用第三方库cmake之旅7编译选项与条件编译cmake之旅8Modern CMake 与 target 思维cmake之旅9安装与导出cmake之旅10自动化测试与 CTestcmake之旅10自动化测试与 CTest上一篇我们学会了把库安装到系统中供他人使用。但一个值得信赖的库需要有一个保障每次修改代码后已有的功能不会被意外破坏。这就是自动化测试的价值。CMake 内置了一个测试驱动工具CTest配合测试框架如 Google Test可以非常方便地在项目中集成单元测试。这一篇我们就来学习这套机制。1 CTest 基础1.1 最简单的测试CTest 不关心你用什么测试框架它只关心一件事运行一个程序检查它的退出码。退出码为 0 表示测试通过非 0 表示测试失败。cmake_minimum_required(VERSION 3.14) project(TestDemo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) # 启用测试功能 enable_testing() # 编译测试程序 add_executable(test_basic test_basic.cpp) # 注册测试名称 要运行的命令 add_test(NAME basic_test COMMAND test_basic)test_basic.cpp#includeiostreamintmain(){intresult23;if(result5){std::cout测试通过std::endl;return0;// 返回 0 表示成功}else{std::cout测试失败std::endl;return1;// 返回非 0 表示失败}}构建并运行测试mkdirbuildcdbuild cmake..makectestctest输出类似Test project /home/user/build Start 1: basic_test 1/1 Test #1: basic_test ....................... Passed 0.00 sec 100% tests passed, 0 tests failed out of 1enable_testing()是关键——它在构建目录中生成 CTest 所需的配置文件。没有这行ctest无法工作。1.2 CTest 常用参数# 显示详细输出ctest--verbose# 简写ctest-V# 只在测试失败时显示输出ctest --output-on-failure# 运行名称匹配正则的测试ctest-Radd.*# 排除匹配的测试ctest-Eslow.*# 并行运行4 个测试同时跑ctest-j41.3 add_test 的更多用法# 基本形式 add_test(NAME my_test COMMAND my_test_exe) # 传递命令行参数 add_test(NAME my_test COMMAND my_test_exe --arg1 --arg2) # 指定工作目录 add_test(NAME my_test COMMAND my_test_exe WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/testdata )还可以为测试设置属性add_test(NAME slow_test COMMAND slow_test_exe) # 设置超时秒 set_tests_properties(slow_test PROPERTIES TIMEOUT 30) # 设置标签可以用 ctest -L unit 只运行带此标签的测试 set_tests_properties(slow_test PROPERTIES LABELS integration) # 期望测试失败用于测试错误处理是否正确 set_tests_properties(should_fail_test PROPERTIES WILL_FAIL TRUE)2 集成 Google Test手动写return 0 / return 1来判断测试结果太原始了。实际项目中我们通常使用专业的测试框架。Google TestGTest是 C 社区最流行的单元测试框架之一。2.1 引入 Google Test最方便的方式是用 FetchContent 自动下载cmake_minimum_required(VERSION 3.14) project(TestDemo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) enable_testing() # 使用 FetchContent 下载 Google Test include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 ) FetchContent_MakeAvailable(googletest)2.2 编写测试假设我们要测试之前的 add 和 de 函数。项目结构├── CMakeLists.txt ├── include │ └── calc │ ├── add.h │ └── de.h ├── src │ ├── add.cpp │ └── de.cpp └── tests ├── CMakeLists.txt ├── test_add.cpp └── test_de.cpptests/test_add.cpp#includegtest/gtest.h#includecalc/add.h// 测试正数相加TEST(AddTest,PositiveNumbers){EXPECT_EQ(add(2,3),5);EXPECT_EQ(add(100,200),300);}// 测试负数相加TEST(AddTest,NegativeNumbers){EXPECT_EQ(add(-1,-1),-2);EXPECT_EQ(add(-5,3),-2);}// 测试零TEST(AddTest,WithZero){EXPECT_EQ(add(0,0),0);EXPECT_EQ(add(5,0),5);}tests/test_de.cpp#includegtest/gtest.h#includecalc/de.hTEST(DeTest,BasicSubtraction){EXPECT_EQ(de(2,10),8);// de(a, b) b - aEXPECT_EQ(de(5,5),0);}TEST(DeTest,NegativeResult){EXPECT_EQ(de(10,2),-8);}tests/CMakeLists.txt# 创建 add 的测试可执行文件 add_executable(test_add test_add.cpp) target_link_libraries(test_add PRIVATE calc_lib GTest::gtest_main) # 创建 de 的测试可执行文件 add_executable(test_de test_de.cpp) target_link_libraries(test_de PRIVATE calc_lib GTest::gtest_main) # 使用 GoogleTest 模块自动发现测试 include(GoogleTest) gtest_discover_tests(test_add) gtest_discover_tests(test_de)关键说明GTest::gtest_main提供了main()函数所以测试文件中不需要自己写main()。只需要写TEST(...)宏就行。gtest_discover_tests比add_test更智能——它会在构建后自动扫描可执行文件中的所有TEST()宏每个 TEST 注册为一个独立的 CTest 测试用例。这意味着ctest会分别报告每个测试的结果而不是笼统地报告整个可执行文件通过或失败。顶层 CMakeLists.txtcmake_minimum_required(VERSION 3.14) project(Calculator VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) # 构建库 add_library(calc_lib src/add.cpp src/de.cpp ) target_include_directories(calc_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) # 测试相关 enable_testing() include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 ) FetchContent_MakeAvailable(googletest) add_subdirectory(tests)2.3 构建并运行mkdirbuildcdbuild cmake..makectest --output-on-failure输出Test project /home/user/build Start 1: AddTest.PositiveNumbers 1/6 Test #1: AddTest.PositiveNumbers .......... Passed 0.00 sec Start 2: AddTest.NegativeNumbers 2/6 Test #2: AddTest.NegativeNumbers .......... Passed 0.00 sec Start 3: AddTest.WithZero 3/6 Test #3: AddTest.WithZero ................. Passed 0.00 sec Start 4: DeTest.BasicSubtraction 4/6 Test #4: DeTest.BasicSubtraction .......... Passed 0.00 sec Start 5: DeTest.NegativeResult 5/6 Test #5: DeTest.NegativeResult ............ Passed 0.00 sec 100% tests passed, 0 tests failed out of 5每个TEST()宏都变成了一个独立的测试用例结果一目了然。3 用 option 控制是否构建测试不是所有使用者都想构建测试下载 Google Test 需要联网而且编译测试也需要额外时间。通常的做法是提供一个开关option(CALC_BUILD_TESTS 构建测试 ON) if(CALC_BUILD_TESTS) enable_testing() include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 ) FetchContent_MakeAvailable(googletest) add_subdirectory(tests) endif()不需要测试时关掉即可cmake-DCALC_BUILD_TESTSOFF..4 BUILD_TESTING 惯例CMake 社区有一个约定使用BUILD_TESTING作为测试开关的变量名。CMake 内置的CTest模块就使用了这个变量# 使用 include(CTest) 代替 enable_testing() # 它会自动定义 BUILD_TESTING 选项默认 ON include(CTest) if(BUILD_TESTING) add_subdirectory(tests) endif()include(CTest)和enable_testing()的区别在于前者会额外定义BUILD_TESTING这个 option并且为 CDash一个测试结果看板系统提供支持。5 Google Test 常用断言最后快速列举一下 Google Test 中常用的断言宏方便你编写测试基本断言失败时终止当前测试宏含义ASSERT_TRUE(cond)条件为真ASSERT_FALSE(cond)条件为假ASSERT_EQ(a, b)a bASSERT_NE(a, b)a ! bASSERT_LT(a, b)a bASSERT_GT(a, b)a b非致命断言失败后继续执行把ASSERT_换成EXPECT_即可如EXPECT_EQ(a, b)。区别是EXPECT_失败后不会终止当前测试而是记录失败并继续执行后面的断言这样一次运行就能看到所有的失败点。建议默认使用EXPECT_系列。浮点数比较// 浮点数不要用 EXPECT_EQ因为精度问题EXPECT_NEAR(0.10.2,0.3,1e-9);// 第三个参数是允许的误差EXPECT_FLOAT_EQ(result,expected);// 约 4 个 ULP 的误差EXPECT_DOUBLE_EQ(result,expected);// 约 4 个 ULP 的误差异常检查EXPECT_THROW(func(),std::runtime_error);// 期望抛出指定异常EXPECT_NO_THROW(func());// 期望不抛出异常6 本篇命令速查表命令作用示例enable_testing()启用 CTest写在顶层 CMakeLists.txt 中include(CTest)启用 CTest 并定义 BUILD_TESTING替代 enable_testingadd_test注册测试用例add_test(NAME my_test COMMAND exe)set_tests_properties设置测试属性set_tests_properties(t PROPERTIES TIMEOUT 30)gtest_discover_tests自动发现 GTest 测试gtest_discover_tests(test_exe)ctest 常用参数参数作用-V/--verbose显示详细输出--output-on-failure只在失败时显示输出-R regex只运行名称匹配的测试-E regex排除名称匹配的测试-jN并行运行 N 个测试-L label只运行指定标签的测试7 总结与下一篇预告这一篇我们学习了 CTest 的基本用法、Google Test 的集成方式、以及测试开关的管理。自动化测试是保证代码质量的基石——每次修改代码后运行ctest就能立刻知道有没有破坏已有功能。到此为止我们的项目已经具备了构建、测试、安装的完整能力。但所有这些都是在本机上进行的。如果你要把代码编译到另一种硬件平台上呢比如你在 x86 电脑上开发但程序要跑在 ARM 嵌入式板子上。这就是交叉编译的场景。下一篇——cmake之旅11交叉编译与工具链文件我们来学习如何用 CMake 进行交叉编译。