新闻详情

新闻详情

首页 / 资讯中心 / 详情

C++单元测试实战:Google Test从环境搭建到CI集成

发布时间:2026/9/26 14:59:27来源:尧图网络
C++单元测试实战:Google Test从环境搭建到CI集成
1. 为什么单元测试这件事值得你花时间啃下 gtest写了几年 C 的人大概都有过这种经历改了一个看似无关紧要的函数编译通过跑起来也没崩结果上线之后某个角落的功能莫名其妙挂了。排查半天才发现是那个函数被另一个模块间接依赖改动破坏了原有的行为契约。这种问题靠人眼 review 很难兜住靠手动点界面测试更是碰运气。单元测试就是用来兜住这类问题的——它把每个函数、每个类的行为固化成可重复执行的断言一旦行为偏离预期立刻报警。Google Test简称 gtest是目前 C 社区里用得最广的单元测试框架之一。它跨平台、语法直观、断言丰富而且和 CMake 集成得非常顺滑。不管你是刚学 C 的新手还是已经在维护几十万行代码的老手gtest 都值得放进工具箱。这篇内容会从环境搭建一路讲到进阶用法包括 Windows 和 Linux 下的安装、CMake 集成、断言体系、测试夹具、参数化测试、死亡测试以及我在实际项目里踩过的那些坑。目标很明确看完之后你能独立给自己的 C 项目搭起一套能跑、能维护、能扩展的测试体系。需要说明的是gtest 本身不依赖任何测试运行器界面它是纯命令行驱动的这意味着它可以无缝接入 CI 流水线。这一点在团队协作里非常关键——测试不是写给自己看的是写给整个团队和自动化系统看的。2. 环境搭建Windows 和 Linux 两条路各有各的坑2.1 Linux 下用包管理器快速起步在 Ubuntu 或 Debian 系上最省事的方式是直接装预编译包sudo apt-get update sudo apt-get install libgtest-dev但这里有个很多人第一次会懵的点libgtest-dev装完之后你拿到的是源码不是编译好的库。早期版本需要你手动进到/usr/src/gtest目录下自己编译cd /usr/src/gtest sudo cmake CMakeLists.txt sudo make sudo cp lib/*.a /usr/lib不过较新的发行版比如 Ubuntu 22.04 之后已经把编译好的库一起打包了装完就能直接用。你可以先检查/usr/lib/x86_64-linux-gnu/下有没有libgtest.a和libgtest_main.a有的话就跳过手动编译这一步。我个人的建议是如果你的项目本身就用 CMake 管理不要依赖系统安装的 gtest而是用FetchContent或add_subdirectory把 gtest 源码拉进项目一起编译。原因很简单——系统装的版本和你项目需要的版本可能不一致换一台机器就可能出问题。把依赖锁在项目里可复现性才有保障。2.2 Windows 下用 CMake 从源码构建Windows 上没有 apt 这种包管理器除非你用 vcpkg 或 MSYS2最稳妥的方式是拉源码自己编。步骤不复杂但有几个细节容易翻车。先克隆源码git clone https://github.com/google/googletest.git cd googletest然后建一个构建目录用 CMake 生成工程mkdir build cd build cmake .. -DCMAKE_INSTALL_PREFIXC:/libs/gtest -G Visual Studio 17 2022 -A x64 cmake --build . --config Release cmake --install . --config Release这里有几个关键点。第一-G指定生成器如果你装的是 VS2022 就写Visual Studio 17 2022装的是 VS2019 就写Visual Studio 16 2019写错了 CMake 会直接报找不到生成器。第二-A x64指定目标架构不写的话默认可能是 Win32后面链接 64 位库的时候会对不上。第三CMAKE_INSTALL_PREFIX指定安装路径建议装到一个没有空格和中文的路径下否则后续 CMake 配置时可能因为路径解析问题报错。注意Windows 下路径里的反斜杠在 CMake 参数里要写成正斜杠或者用双反斜杠转义。C:/libs/gtest这种写法是最省心的。如果你用 vcpkg那就更简单了vcpkg install gtest:x64-windows然后在 CMake 里通过 toolchain 文件引入即可。vcpkg 的好处是依赖管理自动化坏处是初次配置 toolchain 需要一点学习成本。2.3 验证安装是否成功装完之后写一个最小的测试文件验证一下#include gtest/gtest.h TEST(SmokeTest, BasicAssertion) { EXPECT_EQ(1 1, 2); EXPECT_TRUE(true); }编译命令Linux 下直接链接静态库g -stdc17 smoke_test.cpp -lgtest -lgtest_main -lpthread -o smoke_test ./smoke_test如果看到类似[ PASSED ] 2 tests.的输出说明环境没问题。Windows 下如果用 MSVC需要在项目属性里把 gtest 的 include 目录和 lib 目录配好链接gtest.lib和gtest_main.lib。3. CMake 集成让测试成为构建流程的一部分3.1 用 FetchContent 把 gtest 拉进项目这是我现在最推荐的方式没有之一。它把 gtest 的源码在配置阶段自动下载并编译进你的构建树完全不依赖系统环境。cmake_minimum_required(VERSION 3.14) project(MyProject LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 ) set(gtest_force_shared_crt ON CACHE BOOL FORCE) FetchContent_MakeAvailable(googletest) enable_testing() add_executable(my_tests tests/test_main.cpp tests/test_calculator.cpp ) target_link_libraries(my_tests PRIVATE my_library GTest::gtest GTest::gtest_main ) include(GoogleTest) gtest_discover_tests(my_tests)这里有几个值得展开讲的点。gtest_force_shared_crt这个变量在 Windows 下必须设成 ON否则 gtest 用静态 CRT 编译你的项目用动态 CRT链接时会报一堆重复符号或者运行时崩溃。GTest::gtest_main提供了默认的main函数你不需要自己写。gtest_discover_tests是比add_test更现代的方式它在构建后自动扫描测试用例并注册到 CTest新增测试文件不需要手动改 CMake。3.2 目录结构怎么组织才不乱一个常见的做法是把测试代码放在独立的tests目录下和生产代码分开project/ ├── CMakeLists.txt ├── src/ │ ├── calculator.cpp │ └── calculator.h ├── tests/ │ ├── CMakeLists.txt │ ├── test_calculator.cpp │ └── test_main.cpp └── build/顶层 CMakeLists 用add_subdirectory(tests)引入测试子目录。测试子目录里只负责定义测试可执行文件和链接库。这样组织的好处是生产构建可以完全不编译测试通过一个BUILD_TESTING选项控制发布版本不会带上测试代码。option(BUILD_TESTING Build tests ON) if(BUILD_TESTING) enable_testing() add_subdirectory(tests) endif()3.3 跑测试的几种姿势配置好之后跑测试有几种方式# 方式一直接运行测试可执行文件 ./build/tests/my_tests # 方式二通过 CTest 运行 cd build ctest --output-on-failure # 方式三只跑匹配某个模式的测试 ./build/tests/my_tests --gtest_filterCalculatorTest.*--gtest_filter这个参数在调试单个用例时特别好用支持通配符。比如--gtest_filter*Add*会跑所有名字里带 Add 的测试。--output-on-failure让 CTest 只在测试失败时打印完整输出平时保持安静CI 日志会干净很多。4. 断言体系EXPECT 和 ASSERT 到底怎么选4.1 两套断言的本质区别gtest 的断言分两大类EXPECT_*和ASSERT_*。前者失败时继续执行当前测试函数后者失败时直接返回跳过后续代码。这个区别看起来简单但用错了会带来很隐蔽的问题。TEST(AssertionDemo, ExpectVsAssert) { int* ptr nullptr; // EXPECT 版本失败后继续后面的代码会执行 EXPECT_NE(ptr, nullptr); // 这里如果 ptr 是空指针解引用会崩溃 // EXPECT_EQ(*ptr, 42); // 危险 // ASSERT 版本失败后立即返回后面的代码不会执行 ASSERT_NE(ptr, nullptr); EXPECT_EQ(*ptr, 42); // 安全因为 ptr 为空时已经返回了 }原则很简单如果后续代码依赖当前断言的结果用 ASSERT否则用 EXPECT。比如检查指针非空、检查容器大小、检查函数返回值是否有效这些后面要接着用的一律用 ASSERT。纯粹验证行为的用 EXPECT这样一次运行能看到所有失败点而不是修一个跑一次。4.2 常用断言速查断言类型示例用途布尔EXPECT_TRUE(cond)/EXPECT_FALSE(cond)验证条件真假相等EXPECT_EQ(a, b)/EXPECT_NE(a, b)验证值相等或不等比较EXPECT_LT/LE/GT/GE验证大小关系浮点EXPECT_FLOAT_EQ/EXPECT_NEAR(a,b,eps)浮点近似比较字符串EXPECT_STREQ(a, b)/EXPECT_STRCASEEQC 风格字符串比较异常EXPECT_THROW(stmt, type)/EXPECT_NO_THROW验证异常行为死亡EXPECT_DEATH(stmt, regex)验证程序崩溃行为浮点比较是个高频踩坑点。EXPECT_EQ(0.1 0.2, 0.3)会失败因为浮点精度问题。正确做法是用EXPECT_NEAR(0.1 0.2, 0.3, 1e-9)第三个参数是允许的误差范围。这个误差范围怎么定一般取你业务能接受的最小精度比如金融计算用 1e-6图形计算用 1e-4科学计算看具体场景。4.3 自定义失败消息默认的失败消息有时候不够直观尤其是比较两个复杂对象时。gtest 支持用追加自定义信息TEST(UserTest, ValidateAge) { User u{Alice, -5}; EXPECT_GE(u.age, 0) User age is negative: u.age , name: u.name; }失败时会输出你追加的内容排查起来快很多。我的习惯是凡是涉及业务语义的断言都加一句人话描述比如 Order total should not be negative。半年后回来看测试失败不用重新读代码就能知道哪里出了问题。5. 测试夹具多个用例共享初始化逻辑5.1 从 TEST 到 TEST_F当你有一组测试需要相同的初始化数据或环境时用TEST_F配合夹具类Fixture比在每个TEST里重复写初始化代码干净得多。class DatabaseTest : public ::testing::Test { protected: void SetUp() override { db std::make_uniqueDatabase(:memory:); db-execute(CREATE TABLE users (id INT, name TEXT)); } void TearDown() override { db-close(); db.reset(); } std::unique_ptrDatabase db; }; TEST_F(DatabaseTest, InsertUser) { db-execute(INSERT INTO users VALUES (1, Alice)); EXPECT_EQ(db-queryCount(SELECT * FROM users), 1); } TEST_F(DatabaseTest, DeleteUser) { db-execute(INSERT INTO users VALUES (1, Alice)); db-execute(DELETE FROM users WHERE id 1); EXPECT_EQ(db-queryCount(SELECT * FROM users), 0); }每个TEST_F执行时gtest 会创建一个新的夹具实例调用SetUp跑测试体再调用TearDown然后销毁实例。这意味着两个测试之间完全隔离不会互相污染状态。这一点非常重要——测试之间如果有依赖跑的顺序一变结果就变这种测试比没有测试还糟糕。5.2 SetUp 和构造函数该用哪个夹具类里既可以写构造函数也可以写SetUp。区别在于构造函数里抛异常gtest 不会把它当作测试失败处理而是直接崩溃SetUp里抛异常会被捕获并标记为测试失败。所以所有可能失败的初始化逻辑都应该放在SetUp里构造函数只做不可能失败的简单赋值。同理TearDown里做清理析构函数里做兜底释放。如果清理逻辑可能失败比如关闭网络连接放TearDown如果只是释放内存放析构函数也行。5.3 夹具的继承与复用夹具类可以继承这在多层测试场景里很有用class BaseTest : public ::testing::Test { protected: void SetUp() override { logger std::make_uniqueLogger(test.log); } std::unique_ptrLogger logger; }; class NetworkTest : public BaseTest { protected: void SetUp() override { BaseTest::SetUp(); // 别忘了调用基类的 SetUp socket std::make_uniqueSocket(127.0.0.1, 8080); } std::unique_ptrSocket socket; };注意BaseTest::SetUp()这行不能省否则基类的初始化逻辑不会执行。这是新手很容易漏的点而且漏了之后测试可能还是通过的只是没测到该测的东西。6. 参数化测试一份逻辑测多组数据6.1 为什么需要参数化假设你要测一个排序函数输入不同数组验证输出是否正确。用普通TEST你得写一堆重复代码每个用例只有数据不同。参数化测试就是解决这个问题的。class SortTest : public ::testing::TestWithParamstd::vectorint {}; TEST_P(SortTest, SortsAscending) { auto input GetParam(); auto expected input; std::sort(expected.begin(), expected.end()); bubbleSort(input); EXPECT_EQ(input, expected); } INSTANTIATE_TEST_SUITE_P( VariousArrays, SortTest, ::testing::Values( std::vectorint{}, std::vectorint{1}, std::vectorint{2, 1}, std::vectorint{5, 3, 8, 1, 9, 2}, std::vectorint{1, 2, 3, 4, 5} ) );TEST_P里的P代表 Parameterized。INSTANTIATE_TEST_SUITE_P负责把具体数据喂进去。跑的时候 gtest 会为每组数据生成一个独立的测试实例失败时能明确知道是哪组数据挂了。6.2 用 ValuesIn 和 Combine 生成组合除了Values还有几个生成器很实用// 从现有容器生成 std::vectorint cases {1, 2, 3}; INSTANTIATE_TEST_SUITE_P(FromVector, SortTest, ::testing::ValuesIn(cases)); // 生成数值范围 INSTANTIATE_TEST_SUITE_P(Range, SortTest, ::testing::Range(0, 100, 10)); // 组合多个参数 class MathTest : public ::testing::TestWithParamstd::tupleint, int, int {}; INSTANTIATE_TEST_SUITE_P( Combinations, MathTest, ::testing::Combine( ::testing::Values(1, 2, 3), ::testing::Values(10, 20), ::testing::Values(100) ) );Combine会生成笛卡尔积上面这个例子会产生 3×2×16 个测试实例。用的时候注意别组合太多否则测试数量爆炸跑一次要等很久。6.3 参数化测试的命名默认生成的测试名是0、1、2这种索引失败时看不出是哪组数据。可以用第三个参数自定义命名函数INSTANTIATE_TEST_SUITE_P( VariousArrays, SortTest, ::testing::Values(std::vectorint{2, 1}, std::vectorint{3, 1, 2}), [](const ::testing::TestParamInfostd::vectorint info) { std::string name Size; name std::to_string(info.param.size()); return name; } );这样测试名会变成Size2、Size3一眼就能看出区别。命名函数只能返回字母、数字、下划线不能有空格和特殊字符否则编译不过。7. 死亡测试与异常测试验证不该发生的事7.1 死亡测试的适用场景死亡测试用来验证程序在特定输入下会按预期崩溃或退出。听起来有点反直觉但在某些场景下很有用比如验证断言宏在条件不满足时确实会终止程序或者验证某个函数在收到非法参数时会调用abort()。TEST(DeathTest, AssertionFails) { EXPECT_DEATH({ int* p nullptr; *p 42; }, .*); }第二个参数是正则表达式用来匹配崩溃时的错误输出。.*表示匹配任何内容。更精确的写法是匹配具体的错误信息比如Segmentation fault或自定义的断言消息。注意死亡测试在 Windows 下行为和在 Linux 下不完全一致因为两个平台的崩溃信号机制不同。跨平台项目里写死亡测试要格外小心最好在 CI 里分别验证。7.2 异常测试的正确姿势C 里异常测试比死亡测试常用得多TEST(ExceptionTest, ThrowsOnInvalidInput) { Calculator calc; EXPECT_THROW(calc.divide(1, 0), std::invalid_argument); EXPECT_NO_THROW(calc.divide(1, 2)); EXPECT_ANY_THROW(calc.divide(0, 0)); } TEST(ExceptionTest, ExceptionMessage) { Calculator calc; try { calc.divide(1, 0); FAIL() Expected std::invalid_argument; } catch (const std::invalid_argument e) { EXPECT_STREQ(e.what(), Division by zero); } catch (...) { FAIL() Wrong exception type; } }EXPECT_THROW只验证异常类型不验证消息内容。如果要验证消息得用 try-catch 手动捕获。FAIL()宏用来在预期外的路径上强制失败配合 try-catch 使用很顺手。7.3 死亡测试的线程安全警告死亡测试在内部是通过 fork 子进程实现的Linux 下这意味着它和多线程代码一起用会出问题。如果你的测试用例里启动了线程不要在里面写死亡测试否则子进程里的线程状态是未定义的可能死锁或崩溃。这个坑我在一个网络库项目里踩过排查了大半天才定位到是死亡测试和多线程的冲突。8. 那些文档里不会写的实战经验8.1 测试命名要有信息量TEST(Foo, Test1)这种命名等于没命名。好的命名应该让人一眼看出测的是什么行为、在什么条件下、预期什么结果。我习惯用被测对象_场景_预期行为的格式TEST(Calculator_Divide_ByZero_ThrowsException) { ... } TEST(UserService_Register_DuplicateEmail_ReturnsError) { ... }这样测试失败时光看名字就知道哪个功能坏了不用点进去读代码。8.2 一个测试只验证一件事一个TEST里塞十几个断言失败时你只知道有问题不知道具体哪里有问题。更好的做法是拆成多个小测试每个只验证一个行为。gtest 启动测试的开销很小多写几个测试不会明显拖慢速度。8.3 测试数据用工厂函数生成硬编码的测试数据散落在各个测试里改一个字段要改十几个地方。用一个工厂函数集中管理User MakeValidUser() { return User{Alice, 30, aliceexample.com}; } TEST(UserTest, ValidUserPassesValidation) { auto u MakeValidUser(); EXPECT_TRUE(u.isValid()); } TEST(UserTest, InvalidEmailFailsValidation) { auto u MakeValidUser(); u.email not-an-email; EXPECT_FALSE(u.isValid()); }这样默认数据只有一处定义测试里只改需要变化的字段维护成本低很多。8.4 别在测试里写逻辑测试代码里出现 if-else、循环、复杂计算说明测试本身需要被测试了。测试应该是线性的准备数据、执行操作、断言结果。如果发现测试里逻辑太复杂通常意味着被测代码的接口设计有问题或者这个测试想验证的东西太多了。8.5 CI 里跑测试的注意事项在 CI 流水线里跑 gtest有几个参数建议加上./my_tests --gtest_outputxml:test_results.xml --gtest_shuffle --gtest_repeat1--gtest_outputxml生成 JUnit 格式的报告方便 CI 系统解析。--gtest_shuffle打乱测试顺序能暴露测试之间的隐式依赖。--gtest_repeat重复跑多次对排查偶发失败很有用。如果某个测试偶尔挂偶尔过把--gtest_repeat设成 100跑一晚上基本能复现。8.6 测试覆盖率不是越高越好见过一些团队追求 100% 覆盖率结果写了一堆只调用函数但不验证行为的假测试。覆盖率是手段不是目的真正有价值的是测试能否在代码出问题时报警。与其追求数字不如把核心业务逻辑、边界条件、错误路径覆盖到位。getter/setter 这种一行代码的函数不测也罢。9. 从能跑到好用测试体系的持续演进搭起一套能跑的测试只是起点。真正让测试产生长期价值的是把它变成开发流程里自然的一部分。我的做法是每修一个 bug先写一个能复现它的测试再修代码让测试通过。这样 bug 修复后测试留在那里下次同样的问题再出现会立刻被拦住。这个习惯坚持半年测试套件就自然长成了一面防护网。另一个经验是定期清理测试。过时的测试、永远通过的测试、和当前需求不符的测试该删就删。测试套件和代码一样需要维护放任不管只会越来越慢、越来越不可信。我一般每个季度花半天时间过一遍测试列表把没价值的清理掉把重要的补上。gtest 本身的学习曲线很平缓真正难的是养成写测试的习惯和设计可测试的代码结构。后者需要时间但一旦形成肌肉记忆你会发现改代码时心里踏实多了——因为你知道如果改坏了测试会告诉你。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

SpaceX 600亿美元收购Cursor后,AI编程工具配置怎么改?TaoToken统一Key接入Cline与CC Switch 2026/9/26 15:45:30

SpaceX 600亿美元收购Cursor后,AI编程工具配置怎么改?TaoToken统一Key接入Cline与CC Switch

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Skills 乱麻了!TaoToken 统一 Key 让 Cursor/Claude 一键全同步 2026/9/26 15:45:30

Skills 乱麻了!TaoToken 统一 Key 让 Cursor/Claude 一键全同步

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Puppeteer浏览器自动化接入MCP工具:TaoToken统一Key配置与settings.json骨架 2026/9/26 15:45:30

Puppeteer浏览器自动化接入MCP工具:TaoToken统一Key配置与settings.json骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
2026年9月第4周网络安全形势周报 2026/9/26 15:45:23

2026年9月第4周网络安全形势周报

2026年9月第4周网络安全形势周报报告周期: 2026年9月19日—9月25日(第39周)一、本周摘要 本周安全态势呈现"网络边界基础设施集中失守AI代理攻击从理论走向实战供应链攻击规模化"三大主题: CISA KEV单日新增4个已被野外…

阅读更多 →
OpenClaw一键部署真能解放双手?先看清AI接管电脑的代价与TaoToken配置骨架 2026/9/26 15:45:23

OpenClaw一键部署真能解放双手?先看清AI接管电脑的代价与TaoToken配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
OpenBiliClaw功能深度体验:从灵魂画像到朋友式推荐理由,5个必须上手的功能 2026/9/26 15:45:10

OpenBiliClaw功能深度体验:从灵魂画像到朋友式推荐理由,5个必须上手的功能

OpenBiliClaw功能深度体验:从灵魂画像到朋友式推荐理由,5个必须上手的功能 【免费下载链接】OpenBiliClaw 本地私有、开源的自进化跨平台 AI 内容发现 Agent:先理解你,再主动从 B站、小红书、抖音、YouTube、X、知乎、Reddit、微博…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉