ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

googletest完全指南:C++单元测试框架的CMake集成与实战应用

googletest完全指南:C++单元测试框架的CMake集成与实战应用 googletest 是 Google 开源的 C 单元测试框架也是 C 社区里使用最广的测试工具之一。很多知名项目比如 Chromium、LLVM、OpenCV都直接用它来做单元测试或者基于它做二次封装。如果你写 C 代码或者维护一个 C 项目googletest 基本是绕不开的。这篇文章不会讲概念空话直接给你能落地的东西googletest 怎么获取、怎么编译、怎么集成到 CMake 工程、怎么写断言和测试夹具、怎么做参数化测试、怎么跑批量测试用例以及常见的端口冲突、链接失败、测试不生效等问题怎么排查。整个流程都是为了让你的 C 项目快速具备一套可维护、可回归、可自动化的单元测试体系。googletest 最核心的几个特点第一测试用例定义非常简洁TEST 宏几行就能写一个用例第二断言体系完整EXPECT_ 系列和 ASSERT_ 系列覆盖了布尔、数值、字符串、浮点、异常等常见场景第三支持测试夹具 TEST_F 和参数化测试 TEST_P适合做数据驱动第四能原生生成 JUnit 风格的 XML 报告方便接入 CI第五和 CMake 集成度很高可以一键注册所有测试目标。下面会把这几点全部演示一遍。1. googletest 核心能力速览能力项说明项目类型C 单元测试框架开源来源Google 开源维护BSD-3-Clause 许可主要功能单元测试、断言校验、测试夹具、参数化测试、死亡测试、测试过滤、CI 报告输出语言版本较新版本要求 C14 及以上旧版本支持 C11实际以所选 release 要求为准依赖要求需要 CMake 和常见 C 编译器无额外重量级依赖编译方式支持源码编译、包管理器安装、CMake FetchContent 集成是否支持批量任务支持可通过二进制参数一次运行全部用例并输出汇总是否支持接口化可输出 XML 测试报告便于接入 CI 和自动化平台适合场景C/C 服务端组件、算法库、协议层、数据结构与业务逻辑的单元测试与回归测试补充一点googletest 不依赖 GPU没有显存要求CPU 下也能完整运行所以本地部署和学习成本非常低。你只需要一个现代 C 编译器和一套可用的 CMake 环境。2. 适用场景与使用边界googletest 适合以下场景。第一算法模块的单元测试。比如你写了一个字符串处理函数、一个图像缩放函数、一个 JSON 解析器可以用 googletest 把输入和期望输出写进断言每次改动后跑一遍能快速发现回归。第二协议与数据结构的测试。对于网络协议包解析、配置文件解析、内存管理模块googletest 的 EXPECT_EQ、EXPECT_THROW 可以很直观地表达“输入什么、期望什么、异常怎么处理”。第三大型项目的持续集成。googletest 与 CMake 的集成非常顺滑测试用例可以注册到 ctest也可以在 CI 上直接调用测试二进制配合 JUnit 格式报告做质量看板。第四面向接口的代码设计验证。你可以先通过 TEST_F 描述类的外部行为和状态变化再逐步实现功能这就是常见的数据驱动和测试先行工作流。边界也要说清楚。googletest 本质是白盒单元测试框架不适合做端到端 UI 测试、浏览器自动化、跨进程复杂场景编排。这些应该交给 pytest、Selenium、Playwright 等更上层的测试工具。另外googletest 只负责提供断言和测试运行框架不负责 mock 内置对象。虽然它自带 gmock 库但如果你没有用到 mock无需额外引入。在工程化管理上测试代码中涉及真实用户数据、密钥、数据库地址等内容时不要直接写入测试代码并提交到公共仓库。测试用例应该使用脱敏数据和本地模拟环境避免隐私泄露。3. googletest 环境准备与前置条件3.1 编译器与构建工具googletest 是一个源码开源的项目编译门槛不高。你需要准备以下环境。C 编译器GCC、Clang、MSVC 均可推荐使用支持 C14 以上标准的版本。CMake3.14 以上版本更稳妥用于生成构建系统和测试目标。构建工具Linux/macOS 下可以用 Make 或 NinjaWindows 下可以用 Visual Studio 工具链或 Ninja。检查编译器和 CMake 是否可用可以执行下面的命令。g --version cmake --version如果输出正常就说明基础环境没问题。如果你用的是 Windows直接在 Visual Studio Developer Command Prompt 里执行cl和cmake同样可以校验。3.2 获取 googletest 源码建议直接从 GitHub 获取官方仓库。git clone https://github.com/google/googletest.git cd googletest如果没有安装 git也可以从 GitHub Releases 页面下载对应版本的源码压缩包。这里有一点要注意googletest 的 release 版本号和分支名经常变化这篇文章不绑定某一个具体版本你下载当前最新的正式 release 即可。3.3 磁盘空间与目录规划googletest 本身编译后体积不大但为了工程可维护建议目录结构分开管理。比如下面这种布局my_project/ ├── CMakeLists.txt ├── src/ │ └── calculator.cpp ├── include/ │ └── calculator.h ├── tests/ │ ├── CMakeLists.txt │ └── calculator_test.cpp └── build/build目录专门放 CMake 产物测试源码放tests被测代码放src。这样后续批量跑测、清理缓存、接入 CI 都会干净很多。4. 安装部署与编译方式4.1 使用 CMake 编译安装到系统获取源码后在 googletest 目录里执行标准 CMake 流程。cmake -S . -B build -DCMAKE_BUILD_TYPERelease cmake --build build -j8 sudo cmake --install build安装完成后系统里会有 GTest 的库文件和头文件。使用的时候在 CMake 工程里直接find_package(GTest REQUIRED)即可。这种方式适合多个项目共用同一份 googletest。4.2 使用包管理器安装如果你使用 vcpkg、Conan、Homebrew 等包管理器可以直接安装。这里以 vcpkg 为例。vcpkg install gtest cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE[vcpkg-root]/scripts/buildsystems/vcpkg.cmake包管理器的优点是版本由包管理器统一管理升级和卸载都比较方便。缺点是版本更新可能比官方仓库慢如果你需要最新特性可以直接走源码编译。4.3 通过 CMake FetchContent 集成到项目这是目前最推荐的工程化方式。它不需要预先安装 googletestCMake 构建时会自动拉取源码并编译所有项目成员共用同一套依赖锁定。在项目的根CMakeLists.txt里加入以下内容cmake_minimum_required(VERSION 3.14) project(my_project LANGUAGES CXX) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG release-1.12.1 ) FetchContent_MakeAvailable(googletest) add_subdirectory(src) add_subdirectory(tests) enable_testing()这里GIT_TAG建议固定到你验证过的 release 版本不要一直跟随主干分支否则团队不同成员拉取到的代码可能不一致。enable_testing()必须写在添加子目录之后否则后面用gtest_discover_tests注册用例会失效。4.4 单独编译测试二进制的快速验证如果你只想快速跑通一个小例子不一定要完整集成到项目。可以这样写一个临时测试文件然后直接用g编译把 GTest 源码一起编进去。// quick_test.cpp #include gtest/gtest.h TEST(DemoTest, AlwaysTrue) { EXPECT_TRUE(true); } int main(int argc, char** argv) { ::testing::InitGoogleTest(argc, argv); return RUN_ALL_TESTS(); }g quick_test.cpp -stdc14 -I/path/to/googletest/googletest/include \ -L/path/to/googletest/build/lib -lgtest -lgtest_main -pthread -o quick_test ./quick_test-lgtest_main提供了默认的main函数可以省去自己写main的步骤。如果你的项目里已经有一个全局初始化逻辑可以不链接gtest_main自己写入口并调用RUN_ALL_TESTS()。5. 基本测试编写与效果验证5.1 用 TEST 宏写第一个测试用例googletest 最小的测试单元由TEST宏定义。第一个参数是测试套件名第二个参数是测试用例名测试代码直接写在宏体里。#include gtest/gtest.h int Add(int a, int b) { return a b; } TEST(AddTest, HandlesPositiveInput) { EXPECT_EQ(Add(1, 2), 3); } TEST(AddTest, HandlesNegativeInput) { EXPECT_EQ(Add(-1, -2), -3); EXPECT_EQ(Add(-1, 1), 0); }编译运行后可以看到输出类似[] Running 2 tests from 1 test suite. [----------] 2 tests from AddTest [ RUN ] AddTest.HandlesPositiveInput [ OK ] AddTest.HandlesPositiveInput (0 ms) [ RUN ] AddTest.HandlesNegativeInput [ OK ] AddTest.HandlesNegativeInput (0 ms) [----------] 2 tests from AddTest (0 ms total)判断成功的标准很简单所有测试显示OK最后一行是PASSED。如果失败会打印出期望值和实际值。5.2 EXPECT_ 与 ASSERT_ 断言的选择googletest 的断言分为两类。EXPECT_*失败后不终止当前用例会继续执行ASSERT_*失败后直接终止当前用例。这个区别很重要。TEST(AssertDemo, ContinueAfterExpFail) { EXPECT_EQ(1, 2); std::cout 这行会执行 std::endl; } TEST(AssertDemo, StopAfterAssertFail) { ASSERT_EQ(1, 2); std::cout 这行不会执行 std::endl; }在实际测试中如果一个断言失败后后续步骤没有意义就用ASSERT_*。比如先判断指针非空再读取字段指针为空时再读内存可能直接崩溃。如果一个步骤失败后仍想继续收集更多失败信息就用EXPECT_*。常用的断言和验证内容对照如下断言作用EXPECT_TRUE / EXPECT_FALSE判断布尔条件EXPECT_EQ / EXPECT_NE判断相等或不相等可输出左右值差异EXPECT_LT / EXPECT_LE / EXPECT_GT / EXPECT_GE数值大小关系比较EXPECT_STREQ / EXPECT_STRNEC 字符串内容比较不是比较指针EXPECT_FLOAT_EQ / EXPECT_DOUBLE_EQ浮点数近似相等EXPECT_NEAR(a, b, abs_error)浮点数在指定误差范围内相等EXPECT_THROW(expr, exception_type)期望表达式抛出指定异常EXPECT_ANY_THROW / EXPECT_NO_THROW期望有异常或无异常EXPECT_EQ对std::string也可以直接使用因为它重载了输出流。对 C 风格字符串不要用EXPECT_EQ否则比较的是指针地址应该用EXPECT_STREQ。5.3 期望异常与死亡测试函数需要验证异常处理逻辑时EXPECT_THROW可以直接写进断言。假设被测代码是这样int Divide(int a, int b) { if (b 0) { throw std::invalid_argument(division by zero); } return a / b; } TEST(DivideTest, ThrowsOnZeroDivisor) { EXPECT_THROW(Divide(10, 0), std::invalid_argument); EXPECT_NO_THROW(Divide(10, 2)); }注意被测代码必须真实抛出异常断言才会通过。如果你在测试里自己 catch 了异常EXPECT_THROW就无效了。googletest 还支持一种专门的死亡测试用来验证进程会在给定条件下退出比如调用abort()或访问非法内存。常见写法是EXPECT_DEATH或EXPECT_DEATH_IF_SUPPORTED后者在平台不支持时自动跳过兼容性更好。TEST(ProcessTest, DeathWhenAbort) { EXPECT_DEATH_IF_SUPPORTED(std::abort(), Aborted); }死亡测试在 CI 环境里要谨慎使用因为某些环境下子进程启动方式不同需要额外配置。一般业务代码的单元测试里能用异常断言描述的就不要设计成崩溃式死亡测试。5.4 测试夹具 TEST_F 与测试生命周期如果多个测试用例需要共享同一套对象最笨的方式是每个用例里重复创建。更好的方式是用TEST_F配合测试夹具::testing::Test子类。#include gtest/gtest.h #include vector class StackTest : public ::testing::Test { protected: void SetUp() override { for (int i 0; i 3; i) { stack.push_back(i); } } void TearDown() override { stack.clear(); } std::vectorint stack; }; TEST_F(StackTest, SizeIsThreeAfterSetUp) { EXPECT_EQ(stack.size(), 3U); } TEST_F(StackTest, PopReducesSize) { stack.pop_back(); EXPECT_EQ(stack.size(), 2U); }SetUp()在每个用例执行前调用TearDown()在每个用例执行后调用。因此两个测试用例之间不会互相污染stack的数据状态。这是 googletest 最重要的测试隔离机制。这里有一个常见误解TEST_F不是用来替代普通全局函数的它要求第一个参数必须是继承::testing::Test的类名否则编译会报错。如果你只是测一个独立函数不需要共享状态直接用TEST反而更简洁。6. 参数化测试与批量数据处理6.1 TEST_P 与 INSTANTIATE_TEST_SUITE_P测试同一个函数在不同输入下的行为最直接的是复制多个TEST但这样维护成本高。参数化测试TEST_P的作用就是把这组输入和期望输出集中管理。#include gtest/gtest.h class CalculateTest : public ::testing::TestWithParamstd::tupleint, int, int { }; TEST_P(CalculateTest, ShouldAddExpectedValue) { auto params GetParam(); int a std::get0(params); int b std::get1(params); int expected std::get2(params); EXPECT_EQ(a b, expected); } INSTANTIATE_TEST_SUITE_P( AddTestCases, CalculateTest, ::testing::Values( std::make_tuple(1, 2, 3), std::make_tuple(-1, 1, 0), std::make_tuple(100, 200, 300) ) );运行后每个参数组合都会作为一个独立的测试实例运行。如果其中一个参数失败不会影响其他参数这比在一个循环里用EXPECT_EQ检查全部数据更容易定位问题。INSTANTIATE_TEST_SUITE_P的具体参数名和取值方式可以按需调整。::testing::Values适合少数固定输入如果数据量大可以用::testing::Range生成连续数值或者用::testing::ValuesIn传入一个std::vector。6.2 传统数据驱动测试如果你不想引入TestWithParam也可以自己写一个数据表循环在同一个用例里批量验证。这种方式适合测试数据需要动态生成、且任意一条失败时不需要单独统计的场景。TEST(BatchTest, ValidateFromTable) { struct Case { int input; int expected; }; std::vectorCase cases { {0, 0}, {1, 1}, {5, 25}, {-3, 9}, }; for (const auto c : cases) { EXPECT_EQ(c.input * c.input, c.expected) input c.input; } }这里用到了自定义失败信息。一旦断言失败googletest 会额外打印这条信息排查数据时非常有用。批量任务的通用建议是如果每个数据点都希望有独立的成功/失败标记用TEST_P如果只需要整体跑一遍循环写法更轻。6.3 批量跑测与过滤编译出测试二进制后直接运行就能批量执行所有用例。googletest 支持过滤参数比如只跑某个测试套件、只跑名字里带特定关键字的用例。# 运行所有测试 ./calculator_test # 只运行 AddTest 套件 ./calculator_test --gtest_filterAddTest.* # 运行两个套件 ./calculator_test --gtest_filterAddTest*:StackTest.* # 排除部分用例 ./calculator_test --gtest_filter-FloakyTest.*过滤语法里*不是 shell 通配是 googletest 自己解析的模式。多个:表示或关系前面加-表示排除。这个能力在 CI 排障时很实用比如线上报告里只有某个用例失败你可以直接按名字单独跑快速复现。如果想控制失败后的行为可以加参数./calculator_test --gtest_break_on_failure这个参数让用例失败时进入调试器断点适合本地调试。不要把它默认写进 CI否则 CI 会挂起等待调试器。7. CMake 集成与测试报告7.1 将测试注册到 ctest只编译出测试二进制还不够工程化上更常用的是把测试注册到 CTest通过ctest统一管理。在tests/CMakeLists.txt里写add_executable(calculator_test calculator_test.cpp) target_link_libraries(calculator_test PRIVATE calculator_lib gtest_main GTest::gtest ) include(GoogleTest) gtest_discover_tests(calculator_test)其中gtest_discover_tests会解析测试二进制中的所有用例并自动注册到 ctest。它的好处是新增一个测试用例后不需要改 CMake 文件重新构建后ctest自动识别。然后进入构建目录执行cmake -S . -B build cmake --build build -j8 cd build ctest --output-on-failure如果测试失败--output-on-failure会打印对应用例的日志方便定位。如果你在 IDE 里开发注册到 ctest 后也能直接在 IDE 的测试面板里点击运行和调试。7.2 输出 JUnit XML 报告googletest 支持把测试结果输出成 XML 文件方便整合到 Jenkins、GitLab CI 等平台。运行测试二进制时加参数即可。./calculator_test --gtest_outputxml:./report.xml生成的report.xml里包含测试套件名、用例名、耗时、失败信息等内容。CI 系统只要配置好报告路径就能在页面看到历史趋势和失败用例列表。如果你的 CI 是 GitHub Actions可以把--gtest_outputxml:test-results/传进去然后用第三方 action 上传测试报告。注意输出目录要提前创建否则 googletest 可能直接报文件写入失败。8. 性能观察与资源占用googletest 是 CPU 密集型工具没有显存概念但测试工程变大之后资源占用和运行时间依然是需要关注的。运行每个测试时googletest 会统计单个用例耗时。调大日志级别可以看到更详细的耗时./calculator_test --gtest_print_time1默认情况下测试用例运行很快耗时显示为 0 ms。一旦某个用例出现几十毫秒甚至秒级耗时就需要判断是测试代码本身太重还是被测模块性能异常。如果你的项目里有大量容器操作、文件 IO、网络请求这些用例应该单独标记为慢测试避免拖慢整体回归速度。并行跑测是减少总耗时的常用手段。CTest 提供了-j参数可以同时运行多个测试二进制ctest -j4 --output-on-failure要注意并行跑测时如果多个测试共享同一个临时文件或数据库可能产生竞态。最佳实践是每个测试用例使用独立文件路径或者使用测试专用的临时目录。内存方面googletest 本身占用很小。如果被测代码存在内存泄漏建议配合 AddressSanitizer 做检测。编译时加上-fsanitizeaddress运行时越界和泄漏会直接报错。cmake -S . -B build -DCMAKE_CXX_FLAGS-fsanitizeaddress -g cmake --build build ./calculator_test提示AddressSanitizer 与 death test 的组合在某些平台上需要额外配置。第一次跑死机或者测试二进制崩溃时先单独跑对应用例确认是被测代码崩溃还是 ASan 冲突。9. 常见问题与排查方法9.1 编译错误找不到 gtest/gtest.h问题现象可能原因排查方式解决方案编译时提示 gtest/gtest.h 不存在头文件路径没有指定或 googletest 未编译安装检查find_package或 include 路径添加 include 路径或改用 FetchContent 集成链接时提示未定义的testing::*符号缺少-lgtest或-lgtest_main查看链接命令中是否有 gtest 库在 target_link_libraries 中加入 GTest::gtest遇到这类问题先确认库确实编译出来了。源码编译后检查 googletest 的 build 目录里是否存在libgtest.a或libgtest_main.a。如果使用 FetchContent要确认FetchContent_MakeAvailable(googletest)在target_link_libraries之前执行。9.2 测试二进制运行后显示 0 个测试问题现象可能原因排查方式解决方案运行测试文件提示 Running 0 tests忘记调用 RUN_ALL_TESTS或没有链接 gtest_main检查 main 函数链接 gtest_main或在自定义 main 中调用 RUN_ALL_TESTSctest 执行时找不到测试用例gtest_discover_tests 没有正常解析手动运行测试二进制并加--gtest_list_tests确认测试二进制没有依赖动态库加载失败gtest_discover_tests机制下生成的可执行文件必须能被运行时动态加载。如果可执行文件在编译时缺少符号ctest 可能注册 0 个用例。可以先手动在 build 目录里运行./xxx_test --gtest_list_tests验证。9.3 断言总是通过或总是失败问题现象可能原因排查方式解决方案EXPECT_STREQ 比较两个 std::string 失败混淆了字符串类型检查左侧和右侧变量类型用 EXPECT_EQ 比较 std::string或直接比较 c_str()EXPECT_EQ 比较 C 字符串指针通过比较的是指针地址不是内容打印实际指针值改用 EXPECT_STREQ浮点数比较不稳定浮点精度与二进制表示有关打印十六进制或小数位使用 EXPECT_FLOAT_EQ 或 EXPECT_NEAR这类问题最隐蔽。EXPECT_EQ(abc, std::string(abc))的左边是const char*右边是std::string两边类型不一致比较规则可能和预期不一致。建议保持两侧类型相同。9.4 测试之间状态污染问题现象可能原因排查方式解决方案单个用例单独跑通过整体跑失败多个用例共享了全局状态或静态变量用--gtest_filter单独跑某几个用例在 SetUp 和 TearDown 中清理状态避免使用全局变量用例的执行顺序影响结果测试未隔离调整被测代码的静态状态不依赖最后一个用例执行或者为用例提供独立数据目录googletest 不保证测试的执行顺序。如果你的测试用例必须按特定顺序执行说明设计出了问题应该让每个用例独立。9.5 API 或平台差异问题现象可能原因排查方式解决方案在 Windows 下死亡测试不生效平台对子进程支持不同查看 googletest 文档中 death test 说明使用 EXPECT_DEATH_IF_SUPPORTED较老编译器无法编译最新 releaseC 标准要求提高了查看 release notes升级编译器或选用较老版本 GTest如果项目里的编译器版本较老建议在 FetchContent 时固定一个与工具链兼容的 googletest 版本不要直接跟随主分支。10. 最佳实践与使用建议第一次使用 googletest 时先不要追求功能齐全。建议先跑通一个最小用例验证环境没问题再逐步增加测试文件。下面是我觉得比较实用的工程化建议。第一测试目录与源码目录分离。tests目录只放测试代码被测代码单独放到src和include避免测试代码混入生产构建。如果被测模块是库测试二进制链接库目标不要直接编译源码。第二每个测试用例只验证一个行为。如果一个TEST里同时断言了十几种不同场景失败时定位问题会慢一点。更推荐把不同场景拆成不同用例或者用参数化测试管理。第三保持测试代码风格稳定。命名建议使用ModuleTest作为测试套件名用例名使用动宾短语描述行为。比如CalculatorTest、ParsesSimpleJson这样失败报告一眼能看出是哪个模块出了问题。第四写失败信息。所有EXPECT_*后面都可以用追加诊断信息比如输入参数、当前状态、操作步骤。这个信息在批量跑测和 CI 排查时非常关键。第五批量任务要定时跑。单元测试只覆盖代码层面批量回归建议做成 nightly 或每次提交后触发发现失败立即定位。不要把失败的测试搁置超过一天否则修复成本会快速上升。第六注意数据和隐私边界。测试用例里不要使用真实手机号、身份证号、API 密钥。如果测试接口优先用本地 mock 服务或者把敏感配置排除在版本库之外。11. 总结与下一步googletest 最值得尝试的点是它能把 C 项目的“可测试性”快速建立起来。先写一个简单的TEST用例通过 CMake 集成到现有工程再用TEST_F覆盖带状态的对象最后用TEST_P做参数化批量验证这套流程足以覆盖绝大多数单元测试场景。需要最先验证的功能是编译一个包含TEST的测试二进制运行后确认所有用例都能通过。这个流程通了后面的 ctest 注册和 XML 报告就是水到渠成的事。最容易踩的坑是链接问题。很多新手把所有源码直接编进测试二进制导致符号混乱。建议被测代码先打包成库测试二进制只链接库和 GTest这样链接错误会少很多。后续可以继续扩展的方向包括接入 gmock 做接口 mock结合 AddressSanitizer/UBSan 做内存安全检测在 CI 里配置测试阈值和失败通知以及用 ctest 统一调度单元测试和集成测试。先把 googletest 的基础流程跑顺再逐步加这些能力C 工程的质量保障体系会越来越完整。
返回列表