17、Google Test集成:FetchContent引入GTest、gtest_discover_tests、gtest_add_tests

单元测试这件事,说实话,我早年做C++项目时并不太重视。总觉得「代码能跑就行」,直到有一次重构一个核心模块,改了一行看似无关的代码,结果把三个功能同时搞崩了——而且是在上线后才发现的。嗯,从那以后,我再也不敢不写单元测试了。

在CMake生态里,Google Test(简称GTest)是最主流的C++测试框架之一。今天我们就聊聊怎么把它集成到CMake项目里,重点讲三个东西:FetchContent引入GTestgtest_discover_testsgtest_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版本,不要用mastermain。为什么?因为上游随时可能更新,今天能编译的代码,明天可能就断了。
  • FetchContent_MakeAvailable:这个命令会下载、配置、构建GTest,一步到位。
小提示:如果你在公司内网,GitHub访问慢,可以先把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(推荐)

这个命令会在构建完成后,通过运行测试可执行文件来动态发现所有测试用例。它的工作原理是:

  1. 编译测试可执行文件
  2. 运行它,并传入--gtest_list_tests参数
  3. 解析输出,得到所有测试用例名称
  4. 为每个测试用例注册一个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_discover_tests要求测试可执行文件能在构建机器上运行。如果你在做交叉编译,目标平台和构建平台不同,这个命令就行不通了。我曾经在ARM交叉编译环境里吃过这个亏,折腾了半天才发现问题。

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

知识结构图

下面这张图帮你理清整个流程:

Google Test集成流程 CMakeLists.txt 配置 FetchContent 自动下载并构建 GTest 编译测试可执行文件 gtest_discover_tests 运行可执行文件,动态发现测试 gtest_add_tests 解析源码,静态发现测试 推荐使用 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_TIMEOUTTIMEOUT属性,否则CTest默认10秒超时,测试跑不完就被杀了。
核心总结:FetchContent解决依赖管理问题,gtest_discover_tests解决测试注册问题。两者配合,你只需要写测试代码,剩下的CMake帮你搞定。

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