尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

GoogleTest入门指南:C++单元测试框架集成、断言与CI实践

GoogleTest入门指南:C++单元测试框架集成、断言与CI实践 GoogleTest 这个名字做 C 开发的人应该不陌生。它是 Google 开源的 C 单元测试框架GitHub 仓库地址是google/googletest项目里同时包含两大组件gtest负责测试用例编写和运行gmock负责行为模拟通常合称 GoogleTest / Google Mock。这个项目从 2008 年对外发布一直维护到现在许可证是 BSD 3-Clause商用友好也是目前 C 社区使用面最广的测试框架之一。它的核心能力可以概括成一句话用 C 原生语法写断言、建测试夹具、做参数化测试、验证进程崩溃行为、模拟外部依赖然后输出一份 CI 能解析的报告。硬件层面基本没有门槛不需要 GPU不需要特殊设备只要有一台能编译 C 的机器就能跑。真正影响体验的反而是几个工程问题怎么引入依赖、怎么组织用例、怎么接入 CTest、怎么处理链接错误。这篇文章会把 GoogleTest 从“拉到代码”到“跑进 CI”的全过程拆开讲包括 CMake 集成、断言和夹具怎么写、参数化测试和死亡测试怎么用、gMock 怎么模拟外部接口、测试报告怎么输出、常见的编译和链接问题怎么排查。看完之后你可以直接给一个现有 C 工程补上一套可维护、可批量执行、可接 CI 的单元测试体系。1. 核心能力速览先把 GoogleTest 的规格放在前面方便你快速判断这个项目和你的场景匹不匹配。能力项说明项目类型C 单元测试框架含 gtest 与 gmock 两部分开源来源Google 主导GitHub 仓库google/googletest许可证BSD 3-Clause商用和二次开发相对宽松主要功能断言、测试夹具、参数化测试、类型化测试、死亡测试、gMock 行为模拟支持平台Linux、macOS、Windows以及 Android NDK 等交叉编译场景编译器要求GCC / Clang / MSVC 主流版本具体以目标版本 Release Notes 为准构建方式CMake、Bazel社区也维护 vcpkg、Conan 等包管理接入C 标准较早版本支持 C11新版本建议 C14 及以上按实际拉取版本确认报告输出支持 XML、JSON 测试报告供 Jenkins、GitHub Actions 等 CI 解析批量能力支持过滤器、随机顺序、重复执行、分片运行可配合 CTest 并行硬件门槛无特殊要求普通 CPU 即可Googletest 本身占用磁盘空间很小如果你是做 C 库开发、算法模块开发、SDK 回归测试或者正在给老项目补测试GoogleTest 基本是零成本起步的第一选择。2. 适用场景与使用边界GoogleTest 最典型的落地场景是单元测试和组件级回归测试。比如你写了一个字符串解析函数、一个内存池、一个 RPC 客户端封装这些模块边界清晰、输入输出可预期非常适合用TEST或TEST_F写一批断言锁住行为。后续重构时跑一遍全量用例能非常明确地发现“哪个改动把哪个行为改坏了”。它同样适合作为 CI 的质量门禁。通过 CTest 集成后每个提交都可以自动编译并运行全部测试失败时输出--output-on-failure对应的日志并给出 XML/JSON 报告。配合--gtest_shard系列参数还能把测试拆到多个 CI 节点上并行跑解决“测试数量大、单节点跑太久”的问题。但也要注意边界。GoogleTest 解决的是“函数和类的行为是否正确”这一类问题不适合做完整链路 E2E 测试不适合做 GUI 自动化也不适合替代压测工具。跨进程、跨网络、依赖真实环境的场景更适合用专门的集成测试框架或脚本去覆盖。单元测试只能证明“你测过的输入”行为正确测不到的组合依然可能藏 bug所以它不能替代代码审查和手工验证。另外要提一下合规问题。测试代码和被测试代码一样也存在许可证和隐私边界。如果你在项目里二次分发 GoogleTestBSD 3-Clause 要求保留版权声明和免责条款这个要注意。测试中使用的输入数据、Mock 数据、日志样本也必须确认来源合法不要拿未授权的真实用户数据或版权内容直接塞进测试仓库。3. 环境准备与前置条件GoogleTest 不是一个独立运行的软件它是一套需要链接进你测试程序的库。所以“环境准备”实际包括三部分编译器工具链、构建系统、依赖获取方式。操作系统方面Linux、macOS、Windows 都支持。编译器建议用较新的稳定版本GCC、Clang、MSVC 都可以。CMake 版本建议 3.14 以上因为用FetchContent拉取 GoogleTest 需要新版 CMake 的支持如果你只是想手动 clone 源码再编译CMake 版本要求会宽松一些。前置工具清单如下Git用于拉取 googletest 源码。CMake 3.14用于构建和测试发现。可用的 C 编译器例如 GCC、Clang 或 MSVC。一个已有的 C 项目或者一个用来试验的空工程。磁盘占用不用担心。googletest 源码本身在几十 MB 量级构建产物也不会像深度学习框架那样动辄几个 GB。你只需要在网络正常的前提下保证能访问 GitHub 仓库。如果你想用包管理器也可以提前装好 vcpkg 或 Conan。vcpkg 下直接执行vcpkg install gtest就能拿到测试库Conan 里也有对应的gtest包具体版本以你自己的包管理器源为准。有一点需要注意Linux 发行版自带的libgtest-dev通常版本偏旧而且部分发行版只提供源码需要自己用 CMake 编译一次。为了锁定版本、保证 CI 和本地环境一致我更推荐用 CMake 的FetchContent方式。4. 安装部署与构建方式4.1 用 CMake FetchContent 引入这是目前最推荐的接入方式。在CMakeLists.txt里声明依赖CMake 会自己拉取源码、构建静态库、导出 CMake Target整个过程不需要手动去装系统包。cmake_minimum_required(VERSION 3.14) project(MyApp 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 v1.15.2 ) FetchContent_MakeAvailable(googletest) enable_testing() add_executable(unit_tests test_main.cpp test_math.cpp test_stack.cpp ) target_link_libraries(unit_tests PRIVATE GTest::gmock_main GTest::gmock GTest::gtest ) include(GoogleTest) gtest_discover_tests(unit_tests)这里有两个细节要说清楚。第一GIT_TAG填的v1.15.2是当前常见的稳定版标签实际使用前一定要去 GitHub Releases 页面确认你要锁定的版本如果只想用最新主干可以直接填main但稳定性不如固定 tag。第二链接目标GTest::gtest_main会提供一个默认main函数它会自动初始化 GoogleTest 并调用RUN_ALL_TESTS()所以你不需要自己写入口。GTest::gmock_main则是同时支持 gMock 的入口版本功能上是 gtest_main 的超集建议在包含 Mock 用例时直接用它。gtest_discover_tests(unit_tests)的作用是让 CTest 能枚举到可执行文件里的每一个测试用例。这样你执行ctest时每个TEST都会变成 CTest 的一个独立测试项输出粒度更细比“整个二进制跑一遍只输出一个 PASS/FAIL”好用得多。4.2 手动 clone 源码构建如果你不想在 CMake 里自动拉取也可以把 googletest 单独 clone 下来构建。git clone https://github.com/google/googletest.git cd googletest mkdir build cd build cmake .. cmake --build .构建完成之后会在build/lib下生成静态库文件常见的是libgtest.a、libgtest_main.a、libgmock.a、libgmock_main.a。之后你的测试程序手动链接这些库即可。这种方式适合需要把 googletest 做成公共依赖、或者公司内网无法直接访问 GitHub 的场景。4.3 编写第一个测试用例现在写一个最简单的测试文件test_math.cpp#include gtest/gtest.h int Add(int a, int b) { return a b; } TEST(AddTest, HandlesPositiveInput) { EXPECT_EQ(Add(1, 2), 3); EXPECT_GT(Add(2, 3), 4); } TEST(AddTest, HandlesNegativeInput) { EXPECT_EQ(Add(-1, -2), -3); EXPECT_LE(Add(0, 0), 0); }如果没有链接GTest::gtest_main你需要自己提供main#include gtest/gtest.h int main(int argc, char** argv) { ::testing::InitGoogleTest(argc, argv); return RUN_ALL_TESTS(); }编译运行cmake -S . -B build cmake --build build ctest --test-dir build --output-on-failure看到输出里有PASSED或者[ PASSED ] 2 tests.说明你的 GoogleTest 环境已经跑通了。这是整个接入流程最关键的一步后续所有功能都是在“能编译、能运行、能输出报告”的基础上展开的。5. 功能测试与效果验证5.1 断言EXPECT_ 与 ASSERT_GoogleTest 的断言分为两大类EXPECT_*失败后继续执行当前测试ASSERT_*失败后立刻终止当前测试。原则是可恢复的检查用EXPECT_一旦失败后面就没意义的致命条件用ASSERT_。常用断言从语义上分这么几组断言作用EXPECT_EQ / EXPECT_NE判断相等 / 不相等EXPECT_LT / EXPECT_LE / EXPECT_GT / EXPECT_GE大小比较EXPECT_TRUE / EXPECT_FALSE布尔判断EXPECT_FLOAT_EQ / EXPECT_DOUBLE_EQ / EXPECT_NEAR浮点数比较避免直接的精度问题EXPECT_STREQ / EXPECT_STRNE比较const char*指向的字符串内容注意不是指针地址EXPECT_THROW / EXPECT_NO_THROW / EXPECT_ANY_THROW验证是否抛出指定异常 / 不抛异常 / 抛任意异常实际写的时候有一个很常见的坑用EXPECT_EQ去比较两个char*结果比较的是指针地址永远不一致。这种情况要换成EXPECT_STREQ。C 里的std::string重载了用EXPECT_EQ没问题但原生char*必须走EXPECT_STREQ系列。5.2 测试夹具TEST_F当多个测试用例需要相同的准备和清理逻辑时可以用测试夹具。定义一个继承自::testing::Test的类在SetUp()里做初始化在TearDown()里做清理然后所有TEST_F会自动共享这套流程。#include gtest/gtest.h #include vector class VectorTest : public ::testing::Test { protected: void SetUp() override { data {3, 1, 4, 1, 5}; } void TearDown() override { data.clear(); } std::vectorint data; }; TEST_F(VectorTest, SizeMatches) { EXPECT_EQ(data.size(), 5); } TEST_F(VectorTest, FirstElementIsThree) { EXPECT_EQ(data[0], 3); }每个TEST_F执行前都会新建一个夹具对象跑完再销毁所以同一个夹具下的用例之间天然隔离不存在“上一个用例改坏了成员变量影响下一个用例”的问题。注意TEST_F的第一个参数必须是已定义的夹具类名不能是普通字符串。如果整个测试套件只需要做一次的重型初始化可以用SetUpTestSuite()和TearDownTestSuite()它们对应的是测试套件级别的钩子加载模型、分配大内存这类操作适合放这里。5.3 参数化测试TEST_P参数化测试解决的是“同一段逻辑要验证多组输入”的问题。你把测试逻辑写一遍然后通过参数生成器批量喂数据。class IsPositiveTest : public ::testing::TestWithParamint { }; TEST_P(IsPositiveTest, ChecksPositiveValue) { int value GetParam(); EXPECT_GT(value, 0); } INSTANTIATE_TEST_SUITE_P( PositiveValues, IsPositiveTest, ::testing::Values(1, 3, 5, 8, 100) );运行后CTest 里会出现 5 个独立用例分别是PositiveValues/IsPositiveTest.ChecksPositiveValue/0到/4。如果某个参数组合失败报告会直接指出是第几组数据不需要自己去日志里翻。参数生成器除了Values还有Range、ValuesIn、Combine等分别适合离散枚举、连续区间和多个参数笛卡尔积组合。这个能力对边界值测试特别有用比如你有一个函数要验证空字符串、超长字符串、特殊字符等二十种输入用TEST_P写一次逻辑就够了。5.4 类型化测试TYPED_TEST如果被测代码是模板类需要针对int、double、float等多组类型做测试可以用类型化测试。TYPED_TEST_SUITE声明要测试的模板类型列表TYPED_TEST里用TypeParam代表当前类型。template typename T class NumericTest : public ::testing::Test { }; using NumericTypes ::testing::Typesint, float, double; TYPED_TEST_SUITE(NumericTest, NumericTypes); TYPED_TEST(NumericTest, CanConstructAndCompare) { TypeParam value TypeParam(1); EXPECT_TRUE(value 0); }这个功能的价值在于模板代码往往在实例化时才会暴露类型相关的问题。你手动写三份重复测试很容易漏掉其中一个类型TYPED_TEST把类型列表集中管理加类型只是改一行声明的事。5.5 死亡测试EXPECT_DEATH死亡测试用来验证“程序在特定输入下会崩溃、会 abort、会以非零码退出”常用于检查空指针、越界等不可恢复的错误分支。#include gtest/gtest.h #include cstdlib void Foo(int* p) { if (p nullptr) { std::abort(); } } TEST(FooDeathTest, DiesOnNullPointer) { EXPECT_DEATH(Foo(nullptr), .*); }EXPECT_DEATH的第一个参数是要执行的语句第二个参数是正则需要匹配子进程输出到 stderr 的内容。上面的.*只表示“随意匹配”实际项目中建议写成能对应错误信息关键字的正则这样失败时一眼能看出是哪个断言没触发。死亡测试的实现依赖子进程能力POSIX 平台用forkWindows 平台创建子进程。因此它的执行开销比普通测试高一些不要在一个测试套件里堆几千个死亡测试。另外在部分调试器环境里死亡测试可能表现异常排查时可以先单独运行该用例观察输出。5.6 行为模拟gMockgMock 用来模拟被测代码的外部依赖。它不关心这个类怎么实现只定义“当外部依赖被调用时返回什么、调用几次、按什么顺序”。以下面这个例子为例DataLoader是外部依赖MockDataLoader用 gMock 模拟了它的行为#include gmock/gmock.h #include string class DataLoader { public: virtual ~DataLoader() default; virtual int Load(const std::string key) 0; }; class MockDataLoader : public DataLoader { public: MOCK_METHOD(int, Load, (const std::string key), (override)); }; TEST(MockDemo, ReturnsFixedValue) { MockDataLoader loader; EXPECT_CALL(loader, Load(user)) .WillOnce(::testing::Return(42)); EXPECT_EQ(loader.Load(user), 42); }这段代码验证的核心不是返回值本身而是“被测代码调用了Load(user)”这个行为被真实发生。gMock 还支持Times指定调用次数、WillRepeatedly指定多次返回值、InSequence指定调用顺序能力比单纯写桩函数强很多。它特别适合测试网络客户端、数据库访问层、文件系统操作这类不方便在单测里连真实环境的模块。6. 运行、批量执行与 CI 集成6.1 常用运行参数GoogleTest 的可执行文件支持一组以--gtest_开头的运行参数。这些参数在本地调试和 CI 排障时非常关键。# 列出所有测试用例不执行 ./build/unit_tests --gtest_list_tests # 只运行某个测试套件 ./build/unit_tests --gtest_filterVectorTest.* # 运行多个套件排除死亡测试 ./build/unit_tests --gtest_filterVectorTest.*:-*DeathTest* # 随机顺序跑 10 轮适合排查用例间相互依赖的问题 ./build/unit_tests --gtest_shuffle --gtest_repeat10 # 输出 XML 报告 ./build/unit_tests --gtest_outputxml:test_results.xml # 输出 JSON 报告 ./build/unit_tests --gtest_outputjson:test_results.json--gtest_filter支持通配符和排除规则格式是正匹配:负匹配。本地调试时可以先用--gtest_list_tests确认用例全名再复制到--gtest_filter里精准执行。6.2 CTest 批量执行与并行上一节 CMake 配置里的enable_testing()和gtest_discover_tests()已经把测试注册进了 CTest。批量执行统一走ctest --test-dir build --output-on-failure --parallel 4--parallel 4表示同时跑 4 个测试任务适合用例多且彼此独立的场景。--output-on-failure表示只在失败时输出详细日志CI 里看到的是干净的白底绿字失败时立刻定位到具体断言。如果你有多个测试可执行文件ctest会统一调度如果只有一个二进制但用例特别多可以拆成多个测试程序分别注册再用-j并行。6.3 分片执行用例数量大到单台机器跑不完时可以用分片参数把测试拆到多个节点上。# 节点 1 ./build/unit_tests --gtest_total_shards4 --gtest_shard_index0 # 节点 2 ./build/unit_tests --gtest_total_shards4 --gtest_shard_index1CI 平台比如 GitHub Actions 的 matrix可以动态生成这 4 组命令。分片后每个节点只跑四分之一的用例总体执行时间能压到原来的四分之一左右。6.4 GitHub Actions 示例一个最小的 C 项目 CI 配置可以这样写name: unit-tests on: [push, pull_request] jobs: build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Configure run: cmake -S . -B build -DCMAKE_BUILD_TYPERelease - name: Build run: cmake --build build -j2 - name: Test run: ctest --test-dir build --output-on-failure这套配置的核心是三条命令配置、编译、运行测试。编译放在测试之前失败时 CI 直接标红测试阶段输出--output-on-failure配合 GitHub Actions 的日志折叠定位失败用例非常快。7. 资源占用与性能观察GoogleTest 本身对资源没有特殊要求但大规模测试工程在编译和运行阶段会有一些值得观察的点。编译阶段googletest 库本身编译一次之后就不会再变了真正影响增量编译速度的是你的测试文件数量。每个测试文件都包含gtest/gtest.h这个头文件比较重建议按模块拆分测试文件而不是把几千个TEST塞进一个巨型cpp。如果测试工程实在太大可以考虑 PCH预编译头或者 Unity Build把测试源码合并编译来减少重复解析头文件的成本。运行阶段默认情况下所有测试在同一个进程里串行执行每个TEST结束后会清理夹具。资源占用主要看两个指标内存峰值和单用例耗时。这里可以用系统自带的资源监控工具观察也可以直接看ctest输出的耗时字段。死亡测试因为要创建子进程单用例开销明显高于普通用例数量控制在一个合理范围比较稳妥。如果测试涉及真实文件 IO、网络请求或大型容器这类用例即使套了 GoogleTest 也快不了。优先用 gMock 隔离外部依赖让单测跑在纯内存环境里。确实无法消除耗时的重用例建议放到单独的测试程序里用ctest --parallel单独并行避免拖慢其他用例。内存和端口冲突也要留意。GoogleTest 不会自动释放你自己在SetUp里申请的外部资源如果TearDown写得不对长时间跑全量测试可能积累资源泄漏。用一个简单的观察方法重复执行同一组测试两次如果第二次明显变慢或者持续增长内存大概率是清理逻辑有问题。8. 常见问题与排查方法下面是接入 GoogleTest 时最容易遇到的几类问题整理成排查表问题现象可能原因排查方式解决方案编译报错gtest/gtest.h: No such file or directory依赖未拉取或 include 路径缺失检查 build 目录中 googletest 相关源码目录是否存在重新执行FetchContent_MakeAvailable接入路径用 CMake Target 而不是手动 include链接报undefined reference to testing::Test::Test()测试源码没有链接 gtest 库检查target_link_libraries添加GTest::gtest链接链接报undefined reference to main没链接gtest_main也没写自己的 main查看可执行文件入口符号链接GTest::gtest_main或自己写 main 调用RUN_ALL_TESTS()CTest 列表为空但测试能编译CMake 未启用测试发现检查enable_testing/include(GoogleTest)添加enable_testing()和gtest_discover_tests()死亡测试不按预期触发崩溃行为与正则不匹配单独执行该用例观察子进程 stderr调整第二个参数正则确认错误信息关键字EXPECT_EQ比较字符串一直失败比较的是char*指针地址打印两个值确认改用EXPECT_STREQ多个用例互相影响用例间共享全局状态或静态变量用--gtest_shuffle --gtest_repeat复现在SetUp/TearDown中重置状态消除执行顺序依赖FetchContent 拉取 googletest 超时网络到 GitHub 不稳定查看 CMake 日志中的下载地址手动 clone 到本地用SOURCE_DIR指定目录测试通过但进程退出码非零有泄漏检查工具或自定义 main 逻辑查看退出码和 stderr检查main返回语句确认调用了RUN_ALL_TESTS()其中“用例互相影响”是比较隐蔽的一类问题。建议新工程从一开始就遵守两条纪律测试不依赖执行顺序测试不依赖全局状态。否则一旦用例数量上千排查成本会急剧上升。9. 最佳实践与使用建议第一批测试不要贪多。先挑一个边界清晰、输入输出简单的模块写好 5 到 10 个用例把整套 CMake、CTest、CI 流程跑通再逐步扩大覆盖面。这样出现问题时能快速判断是框架接入问题还是业务逻辑问题。测试命名要能表达业务语义。TEST(StringUtilTest, TrimRemovesLeadingAndTrailingSpaces)比TEST(StringUtilTest, Test1)可读性强得多。失败报告里直接就是完整语义不用再对照需求文档猜“这个用例想验证什么”。断言的选用要有原则。一个用例里前置条件用ASSERT_后面所有步骤都依赖这个条件时失败了就该立刻停具体结果校验用EXPECT_这样一次运行能收集到尽可能多的失败信息减少重复排错的次数。把测试用例当作“行为契约”来维护。每次修复 bug先写一个能复现该 bug 的失败用例再修代码让它变绿。这样每个用例对应一条真实的回归场景以后重构时这些用例就是你的安全网。批量任务和接口方面如果你的项目已经有 CI建议把测试分成至少两层一层是纯单测不依赖网络、不依赖数据库、不依赖外部进程跑得快另一层是集成测试可以连接测试环境服务单独控制触发条件。GoogleTest 负责第一层第二层可以继续用这套框架写但在 CMake 里拆成不同 target避免所有环境变量、配置文件都堆在一个可执行文件里。外部依赖一律用 gMock 模拟。真实网络请求、真实文件写入、真实数据库连接都不适合出现在单元测试里。测试环境不稳定会导致用例随机失败CI 的可信度自然就没了。接口边界上只 Mock 被测代码直接依赖的那一层不要连自己也 Mock 了。最后商业项目里接入开源测试框架要检查一遍许可证。GoogleTest 的 BSD 3-Clause 对商用比较友好但如果你在测试代码里引用了其他组件每个组件的许可证都要单独确认。测试代码里不要混入未脱敏的用户数据、密钥、内部地址这类信息一旦进入版本库即使只是测试数据也可能成为安全隐患。10. 总结与下一步如果现在你手里的 C 项目还没有任何测试最值得做的事就是用FetchContent把 GoogleTest 接进来给最核心的工具函数补上第一批TEST然后用ctest跑通一次。整个过程半小时内能完成收益从第一次重构开始就能体现出来。最容易踩的坑集中在三类链接漏了GTest::gtest_main导致没有入口、头文件路径依赖了全局 include 而不是 CMake Target、用例之间共享状态导致顺序依赖。这三类问题在本文第 8 节都有对应排查方式遇到时可以先翻表。接下来可以按这个顺序扩展先把断言和TEST_F用熟再补参数化测试覆盖边界值然后给外部依赖引入 gMock最后把 XML 报告接入 CI配合--gtest_shard做并行分片。等测试规模到几百个用例时你大概率会需要覆盖率工具配合观察这时候再考虑 gcov、lcov 等方案GoogleTest 不会在这条路上成为瓶颈。GoogleTest 最大的价值不是“用了 C 就必须配一个测试框架”这种形式主义而是它让回归测试的成本降到足够低低到你有动力在每次提交前都跑一遍而不是把测试文件堆在那里当摆设。先从一个文件、几个用例开始跑通了后面的事就顺了。
返回列表