尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

GoogleTest 入门指南:从零构建 C++ 单元测试与 CMake 工程

GoogleTest 入门指南:从零构建 C++ 单元测试与 CMake 工程 在 C 项目里单元测试往往不是一开始就有的而是模块反复改、回归问题反复出现后才补上的。GoogleTest通常写作 googletest是 Google 开源的 C 测试框架解决的是让开发者用统一方式编写、组织和运行测试把“功能是否正常”变成可以自动验证的过程。下面从零开始整理 googletest 的常用用法先讲清核心概念再通过一个基于 CMake 的最小工程跑通测试然后介绍测试夹具、参数化测试、运行过滤和常见问题排查最后给出适合真实项目的落地建议。无论你是在维护一个遗留模块还是从第一天就写测试googletest 的断言体系、测试夹具和参数化机制都能直接帮助你把用例写得可读、可维护。文中所有命令和代码都按通用 Linux 场景编写macOS 和 Windows 的差异会在正文标注。1. 先理解 GoogleTest 的核心概念和工作机制1.1 测试用例、测试套件和测试夹具的关系googletest 提供了几个关键抽象测试用例test、测试套件test suite、测试夹具test fixture和断言assertion。很多初学者分不清TEST、TEST_F、TEST_P三个宏原因就是没理解这三者的关系。一个普通测试用例用TEST(SuiteName, TestName)声明例如TEST(CalculatorTest, AddWorks)。这里的CalculatorTest是测试套件名AddWorks是该套件内的具体用例名。同一个套件下的多个测试用例会归为一组运行结果会按套件分组汇总。测试夹具是继承::testing::Test的类通常重写SetUp()和TearDown()方法。TEST_F(FixtureName, TestName)使用这个夹具。每个用例执行前框架都会构造一个全新的夹具对象并调用SetUp()用例结束后调用TearDown()再析构。这样可以确保不同用例之间不共享状态。TEST_P用于参数化测试后面会单独讲。先记住一个核心关系普通测试用TEST带公共初始化的测试用TEST_F需要多组数据驱动的测试用TEST_P。宏测试套件来源是否有夹具适合场景TEST宏的第一个参数无简单、无状态、不需要准备复杂环境的用例TEST_F夹具类名有多个用例需要相同初始化、共享辅助方法的场景TEST_P自定义TestWithParamT类有同一逻辑需要多组输入输出数据的场景从测试设计角度看测试套件名应该描述被测对象的某个特性不要叫Test1、Test2。测试用例名应该描述行为结果比如AddWorksForNegativeNumbers比test1可读得多。1.2 断言为什么比 if 返回值更适合测试googletest 的断言分为两大类EXPECT_*和ASSERT_*。EXPECT_EQ(a, b)如果失败会记录失败信息但继续执行当前用例ASSERT_EQ(a, b)如果失败会立刻终止当前用例。这个设计非常重要EXPECT_*适合检查一组独立条件希望一次跑完看到所有失败ASSERT_*适合检查前置条件前置条件不满足时后续检查已经没有意义。拿一个简单例子TEST(CalculatorTest, AddWorks) { Calculator calc; EXPECT_EQ(calc.Add(1, 2), 3); EXPECT_EQ(calc.Add(2, 2), 4); EXPECT_EQ(calc.Add(0, 0), 0); }如果用if返回值写测试你需要自己拼接错误信息、自己返回失败标记而且通常在第一处错误就结束了。用断言则完全不一样每个断言都知道自己的表达式、期望值、实际值和文件行号失败后能直接打印。这是 googletest 的第一个工程价值它把“测试失败后的诊断成本”降到了最低。你不需要到处打日志失败日志已经告诉你是哪一行、期望什么、实际得到什么。注意虽然EXPECT_*会继续执行但如果后面的语句依赖前面的运算结果仍然会因为空指针或越界导致程序崩溃。这时应该根据依赖关系选择ASSERT_*。2. 环境准备获取 googletest 并配置 CMake 工程2.1 获取源码固定版本比使用主干更可靠在学习环境里最快的方式是直接克隆官方仓库git clone https://github.com/google/googletest.git但在真实项目里不建议使用master分支。googletest 的 API 长期演进中虽然兼容性较好但构建系统和编译选项可能会有变化。更稳妥的做法是选择仓库里已经发布的 tag。例如git clone --branch v1.14.0 --depth 1 https://github.com/google/googletest.git这里以v1.14.0为示例实际落地前要确认项目使用的编译器版本和 googletest 版本匹配。仓库地址和 tag 都以官方 GitHub Releases 为准。2.2 CMake 最小配置FetchContent 与 add_subdirectory在 CMake 工程里推荐使用FetchContent拉取源码这样不依赖系统安装的 googletest 版本也便于锁定版本。cmake_minimum_required(VERSION 3.14) project(DemoTest 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 ) FetchContent_MakeAvailable(googletest) add_library(calculator STATIC calculator.cpp) target_include_directories(calculator PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) add_executable(calculator_test calculator_test.cpp) target_link_libraries(calculator_test PRIVATE calculator gtest_main) include(GoogleTest) gtest_discover_tests(calculator_test)这段配置有几个关键点gtest_main库自带main函数。如果只链接gtest而不链接gtest_main需要自己写main并调用InitGoogleTest。gtest_discover_tests会在构建后自动扫描测试用例比手动add_test更省事。FetchContent_MakeAvailable必须在add_executable之前因为需要先生成 googletest 的 target。如果团队偏好把 googletest 源码放在仓库的third_party目录也可以用add_subdirectory(third_party/googletest)。两种方式各有优势FetchContent适合从远端拉取指定版本add_subdirectory适合需要离线构建的私有环境。2.3 学习环境与生产环境的差异环境获取方式版本控制构建方式学习环境git clone最新稳定 tag一次拉取即可Debug 构建方便阅读断言输出生产环境FetchContent固定 tag或使用包管理器CMake 锁定GIT_TAG提交相关 lockfile更严格的编译选项例如对测试代码单独开启告警控制生产环境还需要考虑测试运行在 CI 中的并行度、输出格式和失败重试机制。这些不是 googletest 框架本身的问题但会影响测试工程的整体质量。注意如果使用系统包管理器安装 googletest不同发行版提供的最低 CMake 版本和头文件安装位置可能不同。优先保证 CI 环境和本地环境使用同一套获取方式。3. 从零写一个最小测试计算器模块和三个用例3.1 被测模块写一个计算器类用于演示普通断言和异常断言。头文件只放声明// calculator.h #ifndef CALCULATOR_H #define CALCULATOR_H #include stdexcept namespace demo { class Calculator { public: int Add(int a, int b); int Divide(int a, int b); }; } // namespace demo #endif // CALCULATOR_H实现文件// calculator.cpp #include calculator.h namespace demo { int Calculator::Add(int a, int b) { return a b; } int Calculator::Divide(int a, int b) { if (b 0) { throw std::invalid_argument(divide by zero); } return a / b; } } // namespace demo这个模块故意保持简单。Divide在除数为零时抛出std::invalid_argument是为了演示EXPECT_THROW的用法。实际项目中除以零可以由上层调用方先做参数校验但作为单元测试示例异常分支是很好的测试对象。3.2 测试文件测试文件放在同一目录用于最小演示// calculator_test.cpp #include gtest/gtest.h #include stdexcept #include calculator.h using demo::Calculator; TEST(CalculatorTest, AddWorksForPositiveNumbers) { Calculator calc; EXPECT_EQ(calc.Add(3, 5), 8); } TEST(CalculatorTest, AddWorksForNegativeNumbers) { Calculator calc; EXPECT_EQ(calc.Add(-3, -5), -8); } TEST(CalculatorTest, DivideByZeroThrows) { Calculator calc; EXPECT_THROW(calc.Divide(1, 0), std::invalid_argument); }这里使用了EXPECT_THROW它验证表达式抛出了指定异常。如果被测代码没有抛异常测试会失败并打印未捕获到异常的信息。Calc对象在每个用例中重新构造因此没有状态污染。这是普通TEST最简单的使用方式用例之间互相独立没有共享成员。3.3 编译、运行与预期输出在项目根目录执行cmake -S . -B build cmake --build build ./build/calculator_test如果一切正常输出大致如下[] Running 3 tests from 1 test suite. [ RUN ] CalculatorTest.AddWorksForPositiveNumbers [ OK ] CalculatorTest.AddWorksForPositiveNumbers (0 ms) [ RUN ] CalculatorTest.AddWorksForNegativeNumbers [ OK ] CalculatorTest.AddWorksForNegativeNumbers (0 ms) [ RUN ] CalculatorTest.DivideByZeroThrows [ OK ] CalculatorTest.DivideByZeroThrows (0 ms) [] 3 tests from 1 test suite ran. (1 ms total) [ PASSED ] 3 tests.从输出中可以看出三个重要信息总用例数、每个用例的执行时间、最终通过状态。这也是 CI 解析测试结果时最常用的字段。如果你希望测试文件能作为单独目标提交到不同构建分组可以把calculator_test.cpp放到test/目录并在test/CMakeLists.txt中引入被测库。这个最小示例采用平铺结构是为了让初学者先理解 googletest 本身的运行链路。4. 测试夹具、参数化测试和类型参数化4.1 TEST_F在每个用例前构造共享环境当多个测试用例需要准备相同的对象、初始化数据或清理操作时应该使用测试夹具而不是在每个TEST里重复代码。一个常见例子是数据库连接、文件句柄或带复杂构造的类。#include gtest/gtest.h #include memory #include calculator.h class CalculatorFixture : public ::testing::Test { protected: void SetUp() override { calc_ std::make_uniquedemo::Calculator(); } void TearDown() override { calc_.reset(); } std::unique_ptrdemo::Calculator calc_; }; TEST_F(CalculatorFixture, AddAfterSetUp) { EXPECT_EQ(calc_-Add(10, 20), 30); }注意两个细节SetUp和TearDown必须放在protected区域否则外部调用者也可能调用它们这是 googletest 对夹具设计的一个约定。夹具成员变量在SetUp里初始化在TearDown里释放。即使测试因为ASSERT_*提前结束TearDown仍然会被调用这比手动写try/finally处理得干净。使用夹具后几个用例之间仍然互不共享状态。每个TEST_F执行前都会新建一个夹具对象SetUp会被重新调用。所以夹具解决的不是“复用同一个对象”而是“复用同一套初始化逻辑”。4.2 TEST_P参数化测试的注册与取值参数化测试适合“同一个行为多组输入输出”的场景。传统写法是复制多个TEST数据一变就要改多处。参数化测试把数据从测试逻辑中分离出来。#include gtest/gtest.h #include tuple #include calculator.h class AddParamTest : public ::testing::TestWithParamstd::tupleint, int, int { }; TEST_P(AddParamTest, AddsTwoNumbers) { auto params GetParam(); int a std::get0(params); int b std::get1(params); int expected std::get2(params); demo::Calculator calc; EXPECT_EQ(calc.Add(a, b), expected); } INSTANTIATE_TEST_SUITE_P( AddCases, AddParamTest, ::testing::Values( std::make_tuple(1, 2, 3), std::make_tuple(-1, 1, 0), std::make_tuple(10, -5, 5), std::make_tuple(0, 0, 0)));INSTANTIATE_TEST_SUITE_P第一个参数是前缀会生成一组带编号的测试名字例如AddCases/AddParamTest.AddsTwoNumbers/0。如果测试失败编号能告诉你具体是哪组参数失败。这里用的是std::tupleint, int, int分别表示左操作数、右操作数和期望结果。你也可以直接用std::pair或自定义结构体只要GetParam()能返回对应类型即可。4.3 类型参数化TYPED_TEST_SUITE 适合模板代码如果被测逻辑是模板同一段测试需要跑在int、double、自定义类型上可以用TYPED_TEST_SUITE和TYPED_TEST。写法相对复杂适合模板库开发者。多数业务项目用TEST_P就足够不必一上来就引入类型参数化。宏/类用途典型场景TEST普通测试无状态函数、简单行为TEST_F带夹具测试需要SetUp/TearDown的初始化逻辑TestWithParamTTEST_P参数化测试同一逻辑多组数据的校验TYPED_TEST_SUITETYPED_TEST类型参数化测试模板代码针对不同类型的验证5. 运行控制过滤、输出格式和调试参数5.1 用 --gtest_filter 控制用例范围全量测试在本地开发时可能太慢可以用过滤参数只跑相关用例。./calculator_test --gtest_filterCalculatorTest.*过滤语法支持*和?通配符也支持冒号分隔多个匹配。./calculator_test --gtest_filterCalculatorTest.*:AddParamTest.*排除部分用例用负向匹配./calculator_test --gtest_filter-*Flaky*在调试某个失败用例时先精确过滤到该用例能减少日志噪声也能确认用例是否单独运行成功。5.2 用 --gtest_output 生成 XML/JSON 报告CI 系统通常不直接解析终端输出而是解析结构化报告。googletest 支持两种格式./calculator_test --gtest_outputxml:test_results.xml ./calculator_test --gtest_outputjson:test_results.json生成的 XML 或 JSON 里包含测试套件名、用例名、状态、运行时间和失败消息。Jenkins 可以直接消费 JUnit 风格的 XMLGitLab CI、GitHub Actions 可以通过对应的测试报告插件或自定义解析。CI 中常见的做法是让每个测试二进制输出独立报告再由上游任务把报告汇总上传。如果多个测试二进制同时写同一个报告文件会产生覆盖问题建议按目标名区分文件。5.3 调试偶发失败repeat、shuffle 和 break-on-failure偶发失败是测试工程里最头疼的问题之一。googletest 提供了几个调试参数./calculator_test --gtest_repeat100 --gtest_shuffle --gtest_break_on_failure--gtest_repeat100表示重复运行 100 次。--gtest_shuffle每次运行打乱用例顺序帮助暴露顺序依赖。--gtest_break_on_failure在第一条失败处断住方便调试器定位。这些参数适合本地复现问题不适合直接放在 CI 的正式流水线里。CI 需要的是可重复、确定的结果偶发失败应该被当作缺陷来看待而不是通过重试掩盖。参数作用常用场景--gtest_filter按套件/用例名过滤本地调试只跑相关用例--gtest_output输出 XML/JSON 报告CI 集成测试报告上传--gtest_repeat重复运行 N 次排查偶发失败--gtest_shuffle打乱执行顺序排查测试互相依赖的状态污染--gtest_break_on_failure失败立即中断调试器断点定位6. 常见编译错误和运行失败排查6.1 链接失败undefined reference to testing::*这是一个高频问题。现象是编译通过链接时报大量undefined reference to testing::...。常见原因有三个target_link_libraries里没有链接gtest或gtest_main。链接了gtest但测试代码里使用了gtest_main提供的main没有链接gtest_main。目标链接顺序不对。静态库的链接顺序很敏感googletest 相关库要放在目标文件之后。检查方式cmake --build build --verbose查看最终链接命令确认链接顺序。处理建议是让 CMaketarget_link_libraries保持target_link_libraries(calculator_test PRIVATE calculator gtest_main)如果自行实现了main则改为链接gtest不要再链接gtest_main。6.2 断言失败时怎么读输出一个失败输出通常长这样[ RUN ] CalculatorTest.AddWorksForPositiveNumbers /workspace/calculator_test.cpp:8: Failure Expected equality of these values: calc.Add(3, 5) Which is: 8 expected Which is: 9 [ FAILED ] CalculatorTest.AddWorksForPositiveNumbers (0 ms)关键信息是文件行号和失败类型。EXPECT_EQ会同时打印两个表达式的值避免你再去手动调试。如果失败信息不直观优先检查EXPECT_*的参数顺序googletest 文档惯例是期望值在前实际值在后。虽然参数顺序不影响编译但会影响失败日志的可读性。6.3 测试偶发失败的三张排查清单排查偶发失败不要第一反应就是加大--gtest_repeat。按下面顺序检查是否依赖了外部资源文件路径、环境变量、网络端口是否在SetUp中写死。是否依赖执行顺序前一个用例是否修改了静态变量、全局缓存或数据库记录。是否使用了随机数随机数种子是否固定并发调度是否影响断言结果。如果确实是顺序依赖可以在TearDown里清理全部状态或者把共享数据改成每个用例独立构造。6.4 至少需要留意的四个常见坑问题现象常见原因检查方式处理建议测试代码找不到gtest/gtest.hinclude 路径未配置确认FetchContent是否成功检查 CMake 顺序链接gtest_main会自动传递 include 路径不要手动加绝对路径TEST_P用例数量不对参数组名重复或INSTANTIATE_TEST_SUITE_P拼写错误运行--gtest_list_tests检查生成的测试名保持前缀唯一EXPECT_EQ比较浮点数偶发失败浮点精度问题查看实际值差异使用EXPECT_NEAR(a, b, abs_error)SetUp里访问未初始化的成员使用普通构造函数而不是SetUp准备测试资源检查测试夹具构造顺序把需要在用例前准备的资源放在SetUp不要在构造函数里调用框架接口7. 工程落地建议与 CI 集成7.1 测试目录与命名规范项目级 googletest 工程通常把测试文件放在test或tests目录与被测源码目录对应。project/ CMakeLists.txt include/ demo/calculator.h src/ calculator.cpp test/ calculator_test.cpp CMakeLists.txt命名规范可以根据团队统一但建议遵循测试文件与被测文件同名后缀加_test.cpp测试套件名使用被测类名用例名使用“动作_期望”式描述例如AddWorksForZeroValue。这样在 CI 报告里看到失败用例名时不需要额外翻代码就能大致判断是哪个行为出了问题。7.2 编写可维护测试的坏味道实践中有几种写法会逐渐拖垮测试工程一个用例塞了一堆无关行为。失败后定位不到具体业务点只能逐行猜。用sleep等待异步结果。稳定性和速度都差优先使用轮询或回调等待。在测试里依赖生产代码的私有实现。被测类内部结构调整测试马上崩。对私有方法写白盒测试。大部分情况下应该通过公共接口验证必要时把测试类声明为friend但不要滥用。只覆盖 happy path。异常分支、边界值和空值输入是回归问题的高发区。7.3 CI 集成前检查清单一个适合进入 CI 的 googletest 测试工程至少应该满足以下条件所有测试不依赖开发者本机环境能一键构建。使用固定版本 googletest不随远端master漂移。测试输出以 XML 或 JSON 上传到 CI 系统失败时能在 MR/PR 上看到具体用例。设置了合理的超时和重试策略避免偶发问题阻塞主干。定义了测试覆盖率门槛但不要求 100% 覆盖率优先保证核心逻辑覆盖。按需在 Linux、Windows、macOS 等平台运行测试避免平台相关的隐性差异。真正写出有价值的 googletest 测试不是靠多写断言而是靠“每个测试都在描述一种行为”。第一次接触时先用TEST写 20 个简单断言理解断言输出再引入TEST_F优化共享代码等数据驱动用例多起来后再把重复的测试逻辑收敛到TEST_P。这三个阶段走完基本就掌握了 googletest 的核心用法。后续可以做 CI 集成、覆盖率分析和失败重试机制从“能跑测试”走向“测试能守住产品质量”。
返回列表