17、Google Test集成:FetchContent引入GTest、gtest_discover_tests、gtest_add_tests
单元测试这件事,说实话,我早年做C++项目时并不太重视。总觉得「代码能跑就行」,直到有一次重构一个核心模块,改了一行看似无关的代码,结果把三个功能同时搞崩了——而且是在上线后才发现的。嗯,从那以后,我再也不敢不写单元测试了。
在CMake生态里,Google Test(简称GTest)是最主流的C++测试框架之一。今天我们就聊聊怎么把它集成到CMake项目里,重点讲三个东西:FetchContent引入GTest、gtest_discover_tests和gtest_add_tests。
为什么不用find_package?
你可能会问:直接用find_package(GTest)不就行了?
理论上可以。但实际项目中,你没法保证每个开发者的机器上都装了GTest。更麻烦的是,版本不一致会导致莫名其妙的编译错误。我遇到过好几次,同事的GTest版本比我低,测试用例跑不过,排查了半天才发现是版本问题。
FetchContent就是来解决这个痛点的。它会在构建时自动从GitHub下载指定版本的GTest,和你的项目一起编译。说白了,就是「依赖随项目走」,版本锁死,谁也别想搞幺蛾子。
FetchContent引入GTest
先看一个完整的CMakeLists.txt示例:
cmake_minimum_required(VERSION 3.14)
project(MyProject LANGUAGES CXX)
# 启用测试
enable_testing()
# 使用FetchContent引入GTest
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)
# 添加你的库
add_library(my_lib src/my_lib.cpp include/my_lib.h)
# 添加测试可执行文件
add_executable(test_my_lib test/test_my_lib.cpp)
target_link_libraries(test_my_lib PRIVATE my_lib gtest gtest_main)
# 注册测试
include(GoogleTest)
gtest_discover_tests(test_my_lib)
这里有几个关键点:
cmake_minimum_required(VERSION 3.14):FetchContent在3.14版本才稳定,低于这个版本会报错。GIT_TAG:我建议你锁死一个具体的release版本,不要用master或main。为什么?因为上游随时可能更新,今天能编译的代码,明天可能就断了。FetchContent_MakeAvailable:这个命令会下载、配置、构建GTest,一步到位。
URL参数指定本地路径。我个人习惯在项目根目录放一个third_party文件夹,里面放好依赖的源码,用FetchContent_Declare(googletest SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/googletest)来引用。
gtest_discover_tests vs gtest_add_tests
这两个命令都是用来注册测试用例的,但工作方式完全不同。我刚开始用的时候也搞混过,后来踩了坑才明白。
gtest_discover_tests(推荐)
这个命令会在构建完成后,通过运行测试可执行文件来动态发现所有测试用例。它的工作原理是:
- 编译测试可执行文件
- 运行它,并传入
--gtest_list_tests参数 - 解析输出,得到所有测试用例名称
- 为每个测试用例注册一个CTest测试
优点很明显:你不需要手动维护测试列表,新增一个测试用例,重新构建一下,CTest自动就能发现它。
gtest_discover_tests(test_my_lib
TEST_PREFIX "my_lib."
TEST_SUFFIX "_test"
DISCOVERY_TIMEOUT 30
PROPERTIES TIMEOUT 60
)
参数说明:
TEST_PREFIX:给每个测试用例加前缀,方便分组。TEST_SUFFIX:加后缀。DISCOVERY_TIMEOUT:发现测试的超时时间。如果你的测试可执行文件启动很慢(比如要加载大模型),可以调大这个值。PROPERTIES TIMEOUT:每个测试用例的运行超时时间。
gtest_add_tests(备选)
这个命令是编译前通过解析源码来发现测试用例的。它不需要运行可执行文件,所以交叉编译场景下也能用。
gtest_add_tests(test_my_lib
TEST_LIST my_tests
)
foreach(test ${my_tests})
set_tests_properties(${test} PROPERTIES TIMEOUT 30)
endforeach()
但它的缺点也很明显:
- 只能解析
TEST()和TEST_F()宏,INSTANTIATE_TEST_SUITE_P生成的参数化测试它发现不了。 - 如果测试用例名称里有特殊字符,解析可能出错。
- 你没法给不同测试用例设置不同的属性(比如超时时间)。
所以我的建议是:能用gtest_discover_tests就用它,只有在交叉编译等特殊场景下才退而求其次用gtest_add_tests。
知识结构图
下面这张图帮你理清整个流程:
一个完整的测试示例
写一个简单的测试文件test/test_my_lib.cpp:
#include <gtest/gtest.h>
#include "my_lib.h"
TEST(MyLibTest, AddPositive) {
EXPECT_EQ(add(1, 2), 3);
EXPECT_EQ(add(100, 200), 300);
}
TEST(MyLibTest, AddNegative) {
EXPECT_EQ(add(-1, -2), -3);
EXPECT_EQ(add(-100, 200), 100);
}
TEST(MyLibTest, StringReverse) {
EXPECT_EQ(reverse("hello"), "olleh");
EXPECT_EQ(reverse(""), "");
}
然后构建并运行测试:
mkdir build && cd build
cmake ..
cmake --build .
ctest --output-on-failure
你会看到类似这样的输出:
Test project /path/to/build
Start 1: MyLibTest.AddPositive
1/3 Test #1: MyLibTest.AddPositive ........... Passed 0.001s
Start 2: MyLibTest.AddNegative
2/3 Test #2: MyLibTest.AddNegative ........... Passed 0.001s
Start 3: MyLibTest.StringReverse
3/3 Test #3: MyLibTest.StringReverse ........ Passed 0.002s
100% tests passed, 0 tests failed out of 3
避坑指南
最后分享几个我踩过的坑:
- GTest版本和编译器不兼容:GTest 1.12.0之后要求C++14,如果你的项目用C++11,记得选老版本。我有个项目就因为没注意这个,在CentOS 7上编译了整整一天没通过。
- FetchContent重复下载:每次
cmake --build都会检查GTest是否已下载,如果网络不稳定,建议用SOURCE_DIR指定本地路径。 - 测试超时:如果你的测试用例涉及大量计算或I/O,记得设置
DISCOVERY_TIMEOUT和TIMEOUT属性,否则CTest默认10秒超时,测试跑不完就被杀了。