news 2026/9/25 1:34:05

gperftools 项目内嵌 GoogleTest 官方示例全解析:从基础断言到 Listener 高级定制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
gperftools 项目内嵌 GoogleTest 官方示例全解析:从基础断言到 Listener 高级定制
  • 性能剖析
  • 内存管理
  • 开发工具

【免费下载链接】gperftools

Main gperftools repository

项目地址:https://gitcode.com/gh_mirrors/gp/gperftools
点击查看免费下载

本指南以 gperftools 仓库中内嵌的 GoogleTest 官方示例文档(samples.md)为骨架,逐一对 Sample #1 ~ #10 进行讲解,并结合 samples/ 目录下的完整源码展开实现细节。gperftools 自身的单元测试(如 src/tests/ 下的 tcmalloc_unittest.cc、page_heap_test.cc 等)正是基于这套内嵌的 GoogleTest 构建与运行的。读完本文,你将能够:理解 GoogleTest 断言、测试固件(Fixture)、类型/值参数化测试的核心用法,掌握通过 Listener API 与反射 API 定制测试输出、实现简易内存泄漏检测的完整套路,并知道如何在 gperftools 仓库中通过 CMake 直接编译运行这些示例。

示例总览与仓库布局

vendor/googletest/docs/samples.md是一个精炼的索引页,它没有长篇论述,而是用 10 条要点概括了 googletest/samples 目录下十个示例各自演示的主题。这十个示例在仓库中的实体文件位于 vendor/googletest/googletest/samples/:

示例主题核心文件(本仓库内)
Sample #1基础步骤:用 GoogleTest 测试 C++ 函数sample1.h、sample1.cc、sample1_unittest.cc
Sample #2对含多个成员函数的类编写更复杂的单元测试sample2.h、sample2_unittest.cc
Sample #3使用测试固件(test fixture)sample3-inl.h、sample3_unittest.cc
Sample #4将 GoogleTest 与googletest.h(即 gtest 头文件)协同使用sample4.h、sample4.cc、sample4_unittest.cc
Sample #5把共享测试逻辑放进基类固件,在派生固件中复用sample5_unittest.cc
Sample #6类型参数化测试(typed tests)sample6_unittest.cc
Sample #7值参数化测试(value-parameterized tests)基础sample7_unittest.cc
Sample #8值参数化测试中使用Combine()sample8_unittest.cc
Sample #9Listener API 修改控制台输出 + 反射 API 检查测试结果sample9_unittest.cc
Sample #10Listener API 实现简易内存泄漏检测器sample10_unittest.cc

其中 Sample #6、#7、#8 共用同一个被测接口 prime_tables.h(PrimeTable接口及OnTheFlyPrimeTable、PreCalculatedPrimeTable两个实现),用来演示“接口测试”(interface tests)——即对同一接口的多个实现跑同一套测试。Sample #3、#5 共用Queue模板类(sample3-inl.h),Sample #1、#5 共用Factorial/IsPrime(sample1.h)。

在 gperftools 仓库的顶层 CMakeLists.txt 中,gtest 被作为静态库编译(CMakeLists.txt),仓库自身的tcmalloc_minimal_unittest、page_heap_test等测试目标均链接了gtest,说明这套内嵌 GoogleTest 就是 gperftools 测试基础设施的一部分;而十个 sample 的编译入口则由 vendor/googletest/googletest/CMakeLists.txt 的gtest_build_samples选项控制。

构建与运行示例的前提

示例默认不参与构建。按照 vendor/googletest/googletest/CMakeLists.txt 中的说明,需要通过-Dgtest_build_samples=ON开启:

# 在 gperftools 仓库根目录执行 cmake -Dgtest_build_samples=ON -B build_samples . cmake --build build_samples --target sample1_unittest sample10_unittest

从 CMake 脚本可见每个示例的链接方式(CMakeLists.txt):

目标链接方式说明
sample1_unittest / sample2_unittest / sample3_unittest / sample4_unittest / sample5_unittestgtest_main链接预置 main 的 gtest_main,免写main()
sample6_unittest / sample7_unittest / sample8_unittestgtest_main同上,依赖prime_tables.h与 prime_tables.h
sample9_unittest / sample10_unittestgtest自行提供main()(用于处理自定义命令行参数)

gtest_main提供的main()只是调用RUN_ALL_TESTS()并返回 0/1(详见 sample1_unittest.cc 的注释),这也是“三步写测试”的最后一步。

Sample #1:三步写出第一个单元测试

Sample #1 演示的是 GoogleTest 最基础的用法。被测函数在 sample1.cc:Factorial(n)返回 n 的阶乘(负数为 1),IsPrime(n)判断素数(先排除 ≤1 和偶数,再从 3 开始只试奇数到平方根为止)。

对应的 sample1_unittest.cc 给出了“1-2-3”三步流程:

  1. 包含必要头文件:#include "gtest/gtest.h"声明测试框架,同时包含被测代码sample1.h;
  2. 用TEST宏定义测试:TEST(FactorialTest, Negative)这样的形式包含两个参数——测试用例名(case/suite 名)与测试名,二者都必须是合法 C++ 标识符且不建议使用下划线;测试逻辑写在宏后的大括号内;
  3. 调用RUN_ALL_TESTS():链接gtest_main即可自动获得入口,无需手工注册测试,宏机制会保证每个测试恰好运行一次,但不保证执行顺序,因此测试结果不应依赖运行次序。

测试体中的断言值得注意(sample1_unittest.cc):

TEST(FactorialTest, Negative) { EXPECT_EQ(1, Factorial(-5)); EXPECT_EQ(1, Factorial(-1)); EXPECT_GT(Factorial(-10), 0); }
  • EXPECT_EQ(expected, actual)等价于EXPECT_TRUE((expected) == (actual)),但失败时会把期望值与实际值同时打印出来,便于调试,因此优先选用;
  • EXPECT_TRUE接受任意布尔表达式,更通用;EXPECT_FALSE同理用于断言为假;
  • 同一用例(case)内可放逻辑相关的多个测试,例如FactorialTest用例下就有Negative、Zero、Positive三个测试,分别覆盖负数、0、正数三条路径;IsPrimeTest用例则覆盖负输入(含INT_MIN)、平凡情形(0/1/2/3)与正输入(含 23 这类素数)。

Sample #2:类的多成员函数测试

Sample #2 演示对一个拥有多个成员函数的类如何组织测试。被测类是MyString(见 sample2.h),其测试分布在 sample2_unittest.cc 的MyString用例下,默认构造函数、接受 C 字符串的构造函数、拷贝构造函数、Set()方法各有一个TEST,即“一个方法对应一个测试”的组织思路。

该示例还展示了两个实用技巧:

  • EXPECT_STREQ(nullptr, s.c_string())用于比较 C 字符串,注意nullptr应写成指针形式(static_cast<const char*>(NULL)亦可),否则EXPECT_EQ因NULL被宏定义为整数0而可能触发编译警告(sample2_unittest.cc);
  • EXPECT_EQ(0, strcmp(...))与EXPECT_EQ(sizeof(kHelloString)/sizeof(kHelloString[0]) - 1, s.Length())这类对字符串长度、内容的组合断言,用于验证Set()在输入指针与对象内部指针相同(自赋值)、置空(Set(nullptr))等边界场景下的行为。

Sample #3:测试固件(Fixture)消除重复代码

Sample #3 引入测试固件(test fixture),其核心思想记录在 sample3_unittest.cc 的注释中:固件是存放一个测试用例内所有测试共享对象与函数的地方,避免为每个测试重复编写初始化与清理代码。被测对象是Queue<E>单链表模板(sample3-inl.h),包含Enqueue、Dequeue、Map等方法。

固件使用四步:

  1. 从testing::Test派生子类(如QueueTestSmpl3),成员用protected修饰以便子类访问;
  2. 重写SetUp()做初始化(在每个测试运行前调用),重写TearDown()做清理(在每个测试结束后调用,非必需);
  3. 在固件中声明测试要用的成员变量与辅助函数;
  4. 用TEST_F(QueueTestSmpl3, TestName)代替TEST定义测试,TEST_F的第一个参数必须是固件类名。

关键设计点(sample3_unittest.cc):

  • 固件是代码共享而非数据共享:每个测试都获得一份全新的固件实例,一个测试修改的数据不会传给下一个测试,这正是“测试应独立、可重复”这一原则的体现——一个测试不应因另一个测试的失败而失败;
  • 断言宏(EXPECT_TRUE、FAIL等)在内部会调用Test类的成员函数以确定“当前测试是谁”,因此不能在全局函数中使用这些宏,需要共享的测试子例程应放在固件内(如示例中的MapTester辅助函数);
  • 固件内可以同时使用ASSERT_EQ(失败即中止当前测试)与EXPECT_EQ(失败后继续执行),例如Dequeue测试先ASSERT_TRUE(n != nullptr)再解引用指针,避免空指针崩溃掩盖后续断言(sample3_unittest.cc)。

Sample #4:被测代码与 GoogleTest 的协作

Sample #4 篇幅最短,但点明了一个集成要点:把被测类(Counter,见 sample4.h)的头文件与gtest/gtest.h一起包含,即可同时获得被测代码声明与测试框架能力。其测试(sample4_unittest.cc)直接验证Increment()/Decrement()的自增自减与边界(0 减 1 仍为 0),并强调EXPECT_EQ()的参数只求值一次,因此可以安全传入带副作用的表达式(如c.Increment()的返回值)。

TEST(Counter, Increment) { Counter c; EXPECT_EQ(0, c.Decrement()); // 0 减 1 仍返回 0 EXPECT_EQ(0, c.Increment()); EXPECT_EQ(1, c.Increment()); EXPECT_EQ(2, c.Increment()); EXPECT_EQ(3, c.Decrement()); }

这一示例的可运行形态是sample4_unittest目标:由 sample4.cc 与gtest_main一起编译链接(CMakeLists.txt)。

Sample #5:基类固件 + 派生固件复用共享逻辑

Sample #5 解决这样一个问题:固件在定义时就绑定了用例名,因此一个固件只能服务一个用例;当多个用例需要相同或相近的固件时,应把共享逻辑放进超类固件(super fixture),再由各用例使用派生固件。

sample5_unittest.cc 中的QuickTest是超类固件:SetUp()记录开始时间,TearDown()断言end_time - start_time <= 5,从而为所有继承它的测试自动施加“单测试不得超过约 5 秒”的约束。随后:

class IntegerFunctionTest : public QuickTest { /* 空体即可 */ }; TEST_F(IntegerFunctionTest, Factorial) { EXPECT_EQ(1, Factorial(-5)); ... }

超类固件本身不需要有同名用例(示例注释明确指出不存在QuickTest用例是正常的)。派生固件还可以继续叠加自己的逻辑——例如针对队列测试的固件在QuickTest之上加入队列数据准备,最终得到“既有超时约束、又有各自初始化”的多层测试固件。该示例同时复用了 Sample #1 的Factorial与 Sample #3 的Queue(sample5_unittest.cc),编译时以gtest_main加 sample1.cc 链接(CMakeLists.txt)。

Sample #6:类型参数化测试(Typed Tests)

Sample #6 演示“对同一接口的多个实现跑同一套测试”(interface tests)。被测接口 prime_tables.h 定义PrimeTable,其两个实现分别为:OnTheFlyPrimeTable(运行时逐个判定素数)与PreCalculatedPrimeTable(构造时预计算并缓存素数表,GetNextPrime超界返回 -1)。

类型参数化测试的写法(sample6_unittest.cc):

  1. 定义固件类模板:template <class T> class PrimeTableTest : public testing::Test,通过模板特化的工厂函数CreatePrimeTable<T>()在构造函数中创建被测对象,并经基类接口PrimeTable*持有——这样测试贴近真实场景,还能避开派生类方法遮蔽基类同名方法(签名略有差异)的陷阱;
  2. 用TYPED_TEST_SUITE(PrimeTableTest, Implementations)声明类型列表,其中Implementations是Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable>类型列表;
  3. 用TYPED_TEST(PrimeTableTest, TestName)定义测试;在测试体内,类型参数以TypeParam引用,固件类以TestFixture引用,且因身处模板世界,访问固件成员必须显式写this->table_;
  4. GoogleTest 会为类型列表中的每个类型自动重复执行每个TYPED_TEST,无需手写循环。

适用于“写测试时已知全部被测类型”的场景;若类型集合是运行时才确定的,则应改用下一节的值参数化测试。

Sample #7:值参数化测试基础

Sample #7 同样做接口测试,但参数是运行时的值(这里是被测对象工厂函数指针)。写法(sample7_unittest.cc):

  1. 从TestWithParam<T>派生固件:class PrimeTableTestSmpl7 : public TestWithParam<CreatePrimeTableFunc*>;
  2. 在固件SetUp()中用GetParam()取参数并创建被测对象(table_ = (*GetParam())();),TearDown()中销毁——每个测试独立创建、销毁被测对象,避免测试间相互影响;
  3. 用TEST_P(PrimeTableTestSmpl7, TestName)定义测试,测试体内同样通过GetParam()访问参数;
  4. 关键一步是实例化:INSTANTIATE_TEST_SUITE_P(OnTheFlyAndPreCalculated, PrimeTableTestSmpl7, Values(&CreateOnTheFlyPrimeTable, &CreatePreCalculatedPrimeTable)),把测试绑定到一组参数值上;可以在不同翻译单元多次实例化。

Values(...)来自::testing::Values,用于枚举离散参数值。未实例化的TEST_P不会被运行,这是与普通测试最大的区别。

Sample #8:用 Combine() 生成参数组合

Sample #8 演示值参数化测试的进阶用法:用Combine()把多组参数两两组合生成笛卡尔积。示例的被测类是HybridPrimeTable(sample8_unittest.cc):它内部同时持有OnTheFlyPrimeTable与PreCalculatedPrimeTable,低内存时可强制只使用前者。

固件参数为TestWithParam< ::std::tuple<bool, int> >,其中bool表示是否强制 on-the-fly,int表示预计算表容量;SetUp()用std::tie解包GetParam()构造被测对象(sample8_unittest.cc)。实例化时:

INSTANTIATE_TEST_SUITE_P(MeaningfulTestParameters, PrimeTableTest, Combine(Bool(), Values(1, 10, 100)));
  • Bool()生成true/false两个取值;
  • Values(1, 10, 100)生成三个容量取值;
  • Combine()生成 2×3=6 个tuple参数,覆盖“预计算表启用/禁用 × 容量大小”的全部组合,从而让同一批TEST_P遍历HybridPrimeTable的所有代码路径(数字落在表容量内/外、表被禁用)。

Sample #9:Listener API 定制输出 + 反射 API 检查结果

Sample #9 展示两条高级能力(sample9_unittest.cc)。

Listener API——替换默认控制台输出:自定义监听器TersePrinter继承EmptyTestEventListener,按需重写生命周期回调:

  • OnTestProgramStart/OnTestProgramEnd:整个测试程序开始/结束(后者打印TEST PASSED/TEST FAILED);
  • OnTestStart/OnTestEnd:单个测试开始/结束,可拿到TestInfo的test_suite_name()与name();
  • OnTestPartResult:某个断言结果产生时触发,从TestPartResult可读取file_name()、line_number()、summary()、failed()等信息,实现“极简输出模式”。

安装与拆卸监听器的标准流程在main()中(sample9_unittest.cc):

UnitTest& unit_test = *UnitTest::GetInstance(); TestEventListeners& listeners = unit_test.listeners(); // 1) 移除默认结果打印器(所有权转移给调用方,需 delete) delete listeners.Release(listeners.default_result_printer()); // 2) 追加自定义监听器(此后所有权归 GoogleTest,无需 delete) listeners.Append(new TersePrinter);

反射 API——枚举并检查测试结果:UnitTest::GetInstance()是单例入口,通过total_test_suite_count()/GetTestSuite(i)遍历用例(TestSuite),再经total_test_count()/GetTestInfo(j)遍历用例内测试(TestInfo),最后用test_info.result()->Failed()判断是否失败。示例据此统计“意外失败”的测试数——名字不含Fails却失败了的测试——并据此修正进程返回值(sample9_unittest.cc)。由于示例自定义了main()处理--terse_output参数,因此编译链接gtest而非gtest_main(CMakeLists.txt)。

运行方式:

./sample9_unittest # 默认输出 ./sample9_unittest --terse_output # 启用 TersePrinter 定制输出

Sample #10:Listener API 实现简易内存泄漏检测

Sample #10 展示 Listener API 的实用价值:实现一个原始的内存泄漏检查器(sample10_unittest.cc)。

思路分三层:

  1. 被监测类型:Water类重载operator new/operator delete,用静态计数器allocated_记录存活对象数(sample10_unittest.cc);
  2. 监听器:LeakChecker : public EmptyTestEventListener重写OnTestStart(记录开始时存活数)与OnTestEnd(比较结束时存活数,若difference > 0则EXPECT_LE(difference, 0) << "Leaked ... unit(s) of Water!"报告泄漏)。注释特别提醒:任何事件处理器中都可以产生失败,唯独OnTestPartResult回调里不行(会递归),所以这里用标准断言即可(sample10_unittest.cc);
  3. 安装:在自定义main()中解析--check_for_leaks参数,为真时向listeners()追加LeakChecker;于是LeaksWater测试(new 了 Water 却未 delete)在开启该开关时会如预期失败,而DoesNotLeak正常通过(sample10_unittest.cc)。
./sample10_unittest --check_for_leaks # 启用自定义泄漏检查

这一“对象计数 + 事件监听”模式在 gperftools 自己的测试中也可见到类似的资源检查思路——仓库中大量内存相关单测(如 src/tests/tcmalloc_unittest.cc、src/tests/page_heap_test.cc)都通过内嵌的 gtest 断言内存分配行为,而 gperftools 自身还提供更专业的内存泄漏检测工具(heap-checker.h),可作为生产环境下的更完整方案。

十个示例的能力矩阵与选型建议

需求选用示例关键宏/API
测试独立函数#1TEST、EXPECT_EQ、EXPECT_TRUE/FALSE、EXPECT_GT
测试类多个成员函数#2TEST+EXPECT_STREQ、strcmp组合断言
多测试共享初始化/清理#3TEST_F、SetUp/TearDown、ASSERT_EQ
被测类与框架头文件协作#4同时包含被测头文件与gtest/gtest.h
跨用例复用固件逻辑#5基类固件 + 派生固件 +TEST_F
已知全部类型的接口测试#6TYPED_TEST_SUITE、TYPED_TEST、Types<>
运行时参数化的接口测试#7TEST_P、INSTANTIATE_TEST_SUITE_P、Values
多参数组合穷举#8Combine、Bool、std::tuple
定制输出与结果检查#9TestEventListener、UnitTest/TestSuite/TestInfo反射遍历
资源泄漏检测#10EmptyTestEventListener+ 对象计数

总的原则:测试应独立、可重复、不依赖执行顺序;断言失败信息要足够定位问题(EXPECT_*优于裸EXPECT_TRUE);共享逻辑放固件而非全局函数;参数化让一套测试逻辑覆盖多个实现或多种参数组合;Listener 与反射 API 则把测试框架从“跑断言”扩展为“构建自定义测试基础设施”。

延伸阅读

  • 官方文档索引与 FAQ:vendor/googletest/docs/index.md、vendor/googletest/docs/faq.md
  • 入门基础:vendor/googletest/docs/primer.md;进阶特性:vendor/googletest/docs/advanced.md
  • GoogleMock 系列:vendor/googletest/docs/gmock_for_dummies.md、vendor/googletest/docs/gmock_cook_book.md、vendor/googletest/docs/gmock_cheat_sheet.md
  • 在 gperftools 仓库中的实际应用:浏览 src/tests/ 目录下的tcmalloc_unittest.cc、page_heap_test.cc、realloc_unittest.cc等,可以看到这些示例所演示的断言、固件与参数化手法如何被真实项目大规模使用;gtest 目标的编译配置见顶层 CMakeLists.txt。
  • 性能剖析
  • 内存管理
  • 开发工具

【免费下载链接】gperftools

Main gperftools repository

项目地址:https://gitcode.com/gh_mirrors/gp/gperftools
点击查看免费下载
上一篇:ResNet-50图像分类实战:从原理到部署的创新路径
下一篇:Xcode 快捷键速查清单:搜索、导航、调试与构建的全流程操作指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 1:32:35

MATLAB仿真对比:OFDM与OTFS在多径衰落信道下的性能验证

简介&#xff1a;本资源面向无线通信与信号处理方向的学习者与研究人员&#xff0c;提供基于OFDM与OTFS两种调制架构的多径衰落信道系统仿真实现&#xff0c;用于对比评估高速移动场景下的传输性能。仿真构建了含5条独立路径的多径信道模型&#xff0c;最大时延扩展2微秒、多普…

作者头像 李华
网站建设 2026/9/25 1:31:58

ESP32运行WebAssembly原理与工程实践

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

作者头像 李华
网站建设 2026/9/25 1:30:51

微信小程序与Spring Boot刷题系统实战:架构拆解与避坑指南

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

作者头像 李华
网站建设 2026/9/25 1:30:48

深入SP3232:双通道TTL转RS232电平转换实战解析

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

作者头像 李华
网站建设 2026/9/25 1:30:27

华为AP4050DN FIT转FAT刷机教程:console线+TFTP自救指南

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

作者头像 李华
网站建设 2026/9/25 1:27:32

嵌入式TIM定时器组件化封装:从原理到实践

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

作者头像 李华