21、CTest测试:集成测试框架

测试这件事,说实话很多C++项目一开始都不太重视。我早年带团队的时候,就吃过没测试的亏——改了一行代码,结果把三个月前的功能搞崩了,排查了两天才找到原因。从那以后,我养成了一个习惯:任何CMake项目,第一件事就是把CTest搭起来。

CTest是CMake自带的测试工具。它不挑测试框架,Google Test、Catch2、Boost.Test都能用。说白了,CTest就是个调度器,帮你跑测试、汇总结果、生成报告。

21.1 开启测试:enable_testing

想用CTest,第一步是在顶层CMakeLists.txt里调用enable_testing()。这个命令必须在add_subdirectory之前调用,否则子目录里的测试加不进来。

cmake_minimum_required(VERSION 3.16)
project(MyApp LANGUAGES CXX)

enable_testing()  # 开启测试支持

add_subdirectory(src)
add_subdirectory(tests)

嗯,这里要注意:enable_testing()只能在顶层CMakeLists.txt里调用一次。子目录里不能用,用了会报错。

⚠️ 常见坑: 如果你在子目录里调了 enable_testing(),CMake 会直接报错退出。我见过有人把测试代码写在子目录里,然后在子目录的 CMakeLists.txt 里写 enable_testing(),结果死活跑不起来。

21.2 添加测试:add_test

测试开启之后,用add_test来注册测试用例。基本用法很简单:

add_test(NAME 测试名称 COMMAND 可执行文件 [参数...])

举个例子:

add_executable(test_math test_math.cpp)
add_test(NAME MathTest COMMAND test_math)

跑测试的时候,在构建目录里执行ctest命令就行:

mkdir build && cd build
cmake ..
cmake --build .
ctest  # 跑所有测试
ctest -R Math  # 只跑名字带 Math 的测试
ctest -N       # 只列出测试,不执行
💡 小技巧: ctest -V 可以输出详细日志,排查失败原因时特别有用。我每次调试测试失败,第一件事就是 ctest -V。

21.3 配合 Google Test

Google Test(简称GTest)是C++社区最流行的单元测试框架之一。CMake官方也提供了FetchContentfind_package来集成GTest。

我个人推荐用FetchContent,因为不用提前安装,项目拉下来就能编译测试。

21.3.1 用 FetchContent 拉取 GTest

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)

这样GTest就集成进来了。然后写测试文件:

// test_math.cpp
#include <gtest/gtest.h>

int add(int a, int b) { return a + b; }

TEST(MathTest, AddPositive) {
  EXPECT_EQ(add(1, 2), 3);
}

TEST(MathTest, AddNegative) {
  EXPECT_EQ(add(-1, -2), -3);
}

在CMakeLists.txt里链接GTest并注册测试:

add_executable(test_math test_math.cpp)
target_link_libraries(test_math GTest::gtest_main)

add_test(NAME MathTest COMMAND test_math)
🔑 关键点: 链接 GTest::gtest_main 会自动生成 main 函数,你只需要写 TEST 宏就行。如果链接 GTest::gtest,需要自己写 main 函数并调用 RUN_ALL_TESTS()。

21.3.2 用 find_package 集成

如果系统里已经安装了GTest,也可以用find_package

find_package(GTest REQUIRED)
add_executable(test_math test_math.cpp)
target_link_libraries(test_math GTest::GTest GTest::Main)
add_test(NAME MathTest COMMAND test_math)

两种方式选一种就行。我个人习惯用FetchContent,因为项目更自包含,别人拉下来就能编译,不用折腾环境。

21.4 测试的组织与分组

项目大了之后,测试用例会很多。CTest支持给测试打标签,方便分组执行:

add_test(NAME MathAddTest COMMAND test_math)
set_tests_properties(MathAddTest PROPERTIES LABELS "math;core")

add_test(NAME StringTest COMMAND test_string)
set_tests_properties(StringTest PROPERTIES LABELS "string;core")

然后可以按标签筛选:

ctest -L core    # 只跑 core 标签的测试
ctest -L math    # 只跑 math 标签的测试
ctest -LE slow   # 排除 slow 标签的测试
💡 实战经验: 我一般把测试分成三个标签:unit(单元测试)、integration(集成测试)、slow(耗时测试)。CI里只跑 unit 和 integration,slow 测试留到晚上跑。

21.5 测试超时与资源限制

有些测试可能会卡住,或者跑太久。CTest支持设置超时:

set_tests_properties(MathTest PROPERTIES TIMEOUT 10)  # 10秒超时

还可以设置测试需要的处理器数量:

set_tests_properties(HeavyTest PROPERTIES PROCESSORS 4)

这样ctest -j4的时候,CTest会合理安排并行度,不会让HeavyTest和其他测试抢CPU。

21.6 知识体系图

下面这张图总结了CTest与Google Test集成的核心流程:

CTest + Google Test 集成流程 CMakeLists.txt enable_testing() FetchContent / find_package(GTest) add_executable + add_test ctest 命令执行测试 测试报告 / 通过 / 失败

21.7 实战建议

最后分享几个我在项目中积累的经验:

  • 测试要快:单元测试应该秒级完成。如果某个测试跑了几分钟,考虑把它标记为slow测试。
  • 测试要独立:每个测试用例之间不要有依赖。我曾经遇到过测试A跑完改了全局状态,测试B就挂了,排查起来非常痛苦。
  • CI里必跑测试:我习惯在CI脚本里加上ctest --output-on-failure,这样测试失败时能直接看到错误日志,不用再手动跑一遍。
  • 覆盖率工具:配合gcov和lcov,可以生成代码覆盖率报告。这个后面章节会详细讲。
📌 一句话总结: enable_testing() 开启测试支持,add_test() 注册测试用例,配合 Google Test 写单元测试,ctest 命令一键执行。这套组合拳,能让你的项目质量提升一个档次。

公众号:蓝海资料掘金营,微信 deep3321