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里调用一次。子目录里不能用,用了会报错。
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 # 只列出测试,不执行
21.3 配合 Google Test
Google Test(简称GTest)是C++社区最流行的单元测试框架之一。CMake官方也提供了FetchContent或find_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)
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 标签的测试
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集成的核心流程:
21.7 实战建议
最后分享几个我在项目中积累的经验:
- 测试要快:单元测试应该秒级完成。如果某个测试跑了几分钟,考虑把它标记为slow测试。
- 测试要独立:每个测试用例之间不要有依赖。我曾经遇到过测试A跑完改了全局状态,测试B就挂了,排查起来非常痛苦。
- CI里必跑测试:我习惯在CI脚本里加上
ctest --output-on-failure,这样测试失败时能直接看到错误日志,不用再手动跑一遍。 - 覆盖率工具:配合gcov和lcov,可以生成代码覆盖率报告。这个后面章节会详细讲。