- 性能剖析
- 内存管理
- 开发工具
【免费下载链接】gperftools
Main gperftools repository
本指南以 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 #9 | Listener API 修改控制台输出 + 反射 API 检查测试结果 | sample9_unittest.cc |
| Sample #10 | Listener 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_unittest | gtest_main | 链接预置 main 的 gtest_main,免写main() |
| sample6_unittest / sample7_unittest / sample8_unittest | gtest_main | 同上,依赖prime_tables.h与 prime_tables.h |
| sample9_unittest / sample10_unittest | gtest | 自行提供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”三步流程:
- 包含必要头文件:
#include "gtest/gtest.h"声明测试框架,同时包含被测代码sample1.h; - 用
TEST宏定义测试:TEST(FactorialTest, Negative)这样的形式包含两个参数——测试用例名(case/suite 名)与测试名,二者都必须是合法 C++ 标识符且不建议使用下划线;测试逻辑写在宏后的大括号内; - 调用
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等方法。
固件使用四步:
- 从
testing::Test派生子类(如QueueTestSmpl3),成员用protected修饰以便子类访问; - 重写
SetUp()做初始化(在每个测试运行前调用),重写TearDown()做清理(在每个测试结束后调用,非必需); - 在固件中声明测试要用的成员变量与辅助函数;
- 用
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):
- 定义固件类模板:
template <class T> class PrimeTableTest : public testing::Test,通过模板特化的工厂函数CreatePrimeTable<T>()在构造函数中创建被测对象,并经基类接口PrimeTable*持有——这样测试贴近真实场景,还能避开派生类方法遮蔽基类同名方法(签名略有差异)的陷阱; - 用
TYPED_TEST_SUITE(PrimeTableTest, Implementations)声明类型列表,其中Implementations是Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable>类型列表; - 用
TYPED_TEST(PrimeTableTest, TestName)定义测试;在测试体内,类型参数以TypeParam引用,固件类以TestFixture引用,且因身处模板世界,访问固件成员必须显式写this->table_; - GoogleTest 会为类型列表中的每个类型自动重复执行每个
TYPED_TEST,无需手写循环。
适用于“写测试时已知全部被测类型”的场景;若类型集合是运行时才确定的,则应改用下一节的值参数化测试。
Sample #7:值参数化测试基础
Sample #7 同样做接口测试,但参数是运行时的值(这里是被测对象工厂函数指针)。写法(sample7_unittest.cc):
- 从
TestWithParam<T>派生固件:class PrimeTableTestSmpl7 : public TestWithParam<CreatePrimeTableFunc*>; - 在固件
SetUp()中用GetParam()取参数并创建被测对象(table_ = (*GetParam())();),TearDown()中销毁——每个测试独立创建、销毁被测对象,避免测试间相互影响; - 用
TEST_P(PrimeTableTestSmpl7, TestName)定义测试,测试体内同样通过GetParam()访问参数; - 关键一步是实例化:
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)。
思路分三层:
- 被监测类型:
Water类重载operator new/operator delete,用静态计数器allocated_记录存活对象数(sample10_unittest.cc); - 监听器:
LeakChecker : public EmptyTestEventListener重写OnTestStart(记录开始时存活数)与OnTestEnd(比较结束时存活数,若difference > 0则EXPECT_LE(difference, 0) << "Leaked ... unit(s) of Water!"报告泄漏)。注释特别提醒:任何事件处理器中都可以产生失败,唯独OnTestPartResult回调里不行(会递归),所以这里用标准断言即可(sample10_unittest.cc); - 安装:在自定义
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 |
|---|---|---|
| 测试独立函数 | #1 | TEST、EXPECT_EQ、EXPECT_TRUE/FALSE、EXPECT_GT |
| 测试类多个成员函数 | #2 | TEST+EXPECT_STREQ、strcmp组合断言 |
| 多测试共享初始化/清理 | #3 | TEST_F、SetUp/TearDown、ASSERT_EQ |
| 被测类与框架头文件协作 | #4 | 同时包含被测头文件与gtest/gtest.h |
| 跨用例复用固件逻辑 | #5 | 基类固件 + 派生固件 +TEST_F |
| 已知全部类型的接口测试 | #6 | TYPED_TEST_SUITE、TYPED_TEST、Types<> |
| 运行时参数化的接口测试 | #7 | TEST_P、INSTANTIATE_TEST_SUITE_P、Values |
| 多参数组合穷举 | #8 | Combine、Bool、std::tuple |
| 定制输出与结果检查 | #9 | TestEventListener、UnitTest/TestSuite/TestInfo反射遍历 |
| 资源泄漏检测 | #10 | EmptyTestEventListener+ 对象计数 |
总的原则:测试应独立、可重复、不依赖执行顺序;断言失败信息要足够定位问题(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
相关推荐
GoogleTest断言机制全面解析:从基础到高级用法
GoogleTest断言机制全面解析:从基础到高级用法 概述 GoogleTest作为C++领域广泛使用的单元测试框架,其断言机制是测试代码的核心组成部分。本文
测试质量保障开发工具Google Test 样例全解析:从基础断言到 Listener 扩展(miniblink49 仓库 v8_7_5 内嵌 gtest 实践指南)
Google Test 样例全解析:从基础断言到 Listener 扩展(miniblink49 仓库 v8_7_5 内嵌 gtest 实践指南) Google
前端桌面应用深入GoogleTest断言系统:从基础到高级技巧
深入GoogleTest断言系统:从基础到高级技巧 本文深入探讨GoogleTest框架中丰富的断言系统,从基础的EXPECT_ 与ASSERT_ 断言区别与应
测试
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考