Google Test 样例全解析:从基础断言到 Listener 扩展(miniblink49 仓库 v8_7_5 内嵌 gtest 实践指南)
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
Google Test(gtest)是 Google 出品的 C++ 单元测试框架,在 miniblink49 仓库中随 V8 7.5 一并内嵌于v8_7_5/testing/gtest,用于支撑 V8 及周边代码的回归测试。本文以官方文档 V1_7_Samples.md 为核心骨架,结合仓库内 10 个官方样例的完整源码,系统讲解 gtest 从入门到进阶的全部核心特性:如何编写第一个单元测试、如何使用测试夹具(Test Fixture)、如何实现类型参数化与值参数化测试,以及如何通过 Listener API 定制输出、检查内存泄漏。读完本文,你将具备直接阅读、运行并复刻这套官方样例的能力,并将其迁移到自己的 C++ 工程中。
一、文档与样例总览:一条完整的 gtest 学习路径
官方文档 V1_7_Samples.md 篇幅精炼,其价值在于为 samples 目录 下 10 个“well-commented”(注释详尽)的样例建立索引。这些样例由浅入深,恰好构成一条完整的 gtest 特性学习路径:
| 样例 | 主题 | 核心 API |
|---|---|---|
| Sample #1 | 函数单元测试的基本三步 | TEST、EXPECT_EQ、EXPECT_TRUE |
| Sample #2 | 含多成员函数类的单元测试 | EXPECT_STREQ |
| Sample #3 | 测试夹具(Test Fixture) | TEST_F、SetUp/TearDown |
| Sample #4 | 基础用例补充:断言只求值一次 | EXPECT_EQ副作用 |
| Sample #5 | 通过派生子夹具复用测试夹具 | 夹具继承、TearDown中断言 |
| Sample #6 | 类型参数化测试(两代写法) | TYPED_TEST/TYPED_TEST_P |
| Sample #7 | 值参数化测试 | TEST_P、TestWithParam、GetParam |
| Sample #8 | Combine()组合多组参数 | INSTANTIATE_TEST_CASE_P+Combine |
| Sample #9 | Listener API 定制控制台输出 + 反射 API | EmptyTestEventListener、UnitTest |
| Sample #10 | Listener API 实现简易内存泄漏检查器 | 自定义事件监听器 |
注意:本文所述 API 形态(如
TEST_CASE系命名)以仓库内嵌的 1.7 版本为准,与新版本 gtest(1.8+ 改用TEST_SUITE命名)存在差异,下文会同步指出新旧命名对应关系。
二、Sample #1:单元测试“三步走”,掌握最小可运行测试
Sample #1 展示如何为一个普通 C++ 函数编写单元测试,被被测对象是 sample1.h / sample1.cc 中的Factorial()(阶乘)与IsPrime()(素数判定)。源码注释把写作流程归纳为清晰的 1-2-3:
Step 1:包含头文件
测试代码需要包含被测目标的声明,同时必须引入框架头文件gtest/gtest.h(实际路径为 v8_7_5/testing/gtest/include/gtest/gtest.h):
#include <limits.h> #include "sample1.h" #include "gtest/gtest.h"Step 2:用 TEST 宏定义测试
TEST宏接收两个参数:测试用例名(test case name)与测试名(test name),随后在花括号内书写测试逻辑:
TEST(FactorialTest, Negative) { EXPECT_EQ(1, Factorial(-5)); EXPECT_EQ(1, Factorial(-1)); EXPECT_GT(Factorial(-10), 0); } TEST(FactorialTest, Zero) { EXPECT_EQ(1, Factorial(0)); } TEST(FactorialTest, Positive) { EXPECT_EQ(1, Factorial(1)); EXPECT_EQ(2, Factorial(2)); EXPECT_EQ(6, Factorial(3)); EXPECT_EQ(40320, Factorial(8)); }源码注释给出了三条重要约定,值得注意:
- 测试按用例分组:逻辑相关的测试放入同一 test case(如
FactorialTest),用于保持测试代码的组织性; - 命名约束:用例名与测试名都必须是合法 C++ 标识符,且不要在其中使用下划线(
_)——下划线是 gtest 内部命名空间预留的; - 顺序无关性:gtest 保证每个测试恰好被执行一次,但不保证执行顺序,因此测试必须写成结果不依赖执行顺序的形式。
断言宏的选择:EXPECT_EQ 优于 EXPECT_TRUE
针对IsPrime(),样例分别覆盖负数、平凡值(0、1、2、3)与正数输入:
TEST(IsPrimeTest, Negative) { EXPECT_FALSE(IsPrime(-1)); EXPECT_FALSE(IsPrime(-2)); EXPECT_FALSE(IsPrime(INT_MIN)); } TEST(IsPrimeTest, Trivial) { EXPECT_FALSE(IsPrime(0)); EXPECT_FALSE(IsPrime(1)); EXPECT_TRUE(IsPrime(2)); EXPECT_TRUE(IsPrime(3)); } TEST(IsPrimeTest, Positive) { EXPECT_FALSE(IsPrime(4)); EXPECT_TRUE(IsPrime(5)); EXPECT_FALSE(IsPrime(6)); EXPECT_TRUE(IsPrime(23)); }注释中特别解释了EXPECT_EQ(expected, actual)与EXPECT_TRUE((expected) == (actual))的等价关系:二者行为等价,但EXPECT_EQ在失败时会同时打印期望值与实际值,对调试极有帮助,因此优先使用;而EXPECT_TRUE接受任意布尔表达式,通用性更强。此外还有带ASSERT_前缀的兄弟宏(见下节),失败时会直接终止当前测试。
Step 3:调用 RUN_ALL_TESTS()
所有测试由RUN_ALL_TESTS()统一驱动。样例通过链接 src/gtest_main.cc 获得现成的main()——该文件内部即包含调用RUN_ALL_TESTS()的入口函数,运行全部已定义测试、打印结果,成功返回 0、失败返回 1。注意你不需要手工注册任何测试,RUN_ALL_TESTS()宏会自动感知所有已定义测试。
三、Sample #2 与 Sample #4:类的成员函数测试与断言副作用
为类的每个成员函数建立测试
Sample #2 演示对 sample2.h / sample2.cc 中MyString类的单元测试。惯例是为类的每个成员函数各建一个测试,保持组织清晰:
TEST(MyString, DefaultConstructor) { const MyString s; EXPECT_STREQ(NULL, s.c_string()); EXPECT_EQ(0u, s.Length()); } TEST(MyString, ConstructorFromCString) { const MyString s(kHelloString); EXPECT_EQ(0, strcmp(s.c_string(), kHelloString)); EXPECT_EQ(sizeof(kHelloString)/sizeof(kHelloString[0]) - 1, s.Length()); } TEST(MyString, CopyConstructor) { const MyString s1(kHelloString); const MyString s2 = s1; EXPECT_EQ(0, strcmp(s2.c_string(), kHelloString)); } TEST(MyString, Set) { MyString s; s.Set(kHelloString); EXPECT_EQ(0, strcmp(s.c_string(), kHelloString)); s.Set(s.c_string()); // 输入指针与内部指针相同也必须正确工作 EXPECT_EQ(0, strcmp(s.c_string(), kHelloString)); s.Set(NULL); EXPECT_STREQ(NULL, s.c_string()); }这里引入了两个新知识点:
EXPECT_STREQ:用于 C 字符串(const char*)的相等断言,而非EXPECT_EQ(后者比较的是指针值);NULL的类型陷阱:源码注释专门解释了一个隐蔽问题——若直接写EXPECT_EQ(NULL, ...),由于NULL被宏定义为整数 0,编译器会把格式化函数按int推导,而 gcc 3.4 会因“NULL 应作为指针而非 int”而告警。根因是 C++ 无法区分整数 0 与空指针常量,因此需要写成static_cast<const char*>(NULL)或改用EXPECT_STREQ。
断言中的副作用:EXPECT_EQ 恰好求值一次
Sample #4 是一个极简补充样例,测试 sample4.h / sample4.cc 中的计数器类:
TEST(Counter, Increment) { Counter c; EXPECT_EQ(0, c.Increment()); EXPECT_EQ(1, c.Increment()); EXPECT_EQ(2, c.Increment()); }注释明确指出:EXPECT_EQ()的实参恰好被求值一次,因此实参可以安全地带有副作用(此处Increment()每次调用都会修改状态),三次断言依次验证 0→1→2 的递增行为。这为在断言内书写有副作用的表达式提供了依据。
四、Sample #3:测试夹具(Test Fixture)与 TEST_F
当多个测试需要共享初始化对象与公共子例程时,若直接复制粘贴会产生大量重复代码。Sample #3 展示 gtest 的解法——测试夹具,被测对象是 sample3-inl.h 中的Queue<int>模板队列。
定义夹具:从 testing::Test 派生
class QueueTest : public testing::Test { protected: virtual void SetUp() { q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); } static int Double(int n) { return 2*n; } void MapTester(const Queue<int> * q) { const Queue<int> * const new_q = q->Map(Double); ASSERT_EQ(q->Size(), new_q->Size()); for (const QueueNode<int> * n1 = q->Head(), * n2 = new_q->Head(); n1 != NULL; n1 = n1->next(), n2 = n2->next() ) { EXPECT_EQ(2 * n1->element(), n2->element()); } delete new_q; } Queue<int> q0_; Queue<int> q1_; Queue<int> q2_; };要点如下:
- 夹具类必须从
testing::Test公开派生,成员建议声明为protected以便子类访问; SetUp()在每个测试运行前被调用,用于初始化共享变量(本例向队列预置数据);无初始化需求时可省略;TearDown()在每个测试结束后被调用,用于清理;无清理需求时可省略(样例中以注释形式给出空实现);- 夹具中可以定义供测试复用的子例程(如
MapTester)与共享对象(如q0_、q1_、q2_)。
用 TEST_F 编写夹具测试
有夹具后,测试改用TEST_F定义:
TEST_F(QueueTest, DefaultConstructor) { EXPECT_EQ(0u, q0_.Size()); } TEST_F(QueueTest, Dequeue) { int * n = q0_.Dequeue(); EXPECT_TRUE(n == NULL); n = q1_.Dequeue(); ASSERT_TRUE(n != NULL); EXPECT_EQ(1, *n); EXPECT_EQ(0u, q1_.Size()); delete n; n = q2_.Dequeue(); ASSERT_TRUE(n != NULL); EXPECT_EQ(2, *n); EXPECT_EQ(1u, q2_.Size()); delete n; } TEST_F(QueueTest, Map) { MapTester(&q0_); MapTester(&q1_); MapTester(&q2_); }两个关键设计原理
源码注释揭示了 gtest 的两条核心设计哲学,理解它们才能写出正确测试:
- 夹具是“代码共享”而非“数据共享”:每个测试都会获得一份全新的、独立的夹具副本,一个测试修改的数据不会传递给下一个测试。原因在于测试必须相互独立且可重复——一个测试不应因另一个测试的失败而失败;若两个测试确实依赖彼此产生的数据,它们本该合并成一个更大的测试。
- 断言宏只能在测试内使用:
EXPECT_*、FAIL等宏在技术上是Test类的成员函数,gtest 打印失败信息时需要知道“当前是哪个测试”。因此在全局函数中无法使用它们——这也是测试子例程必须放进夹具的原因(MapTester内部使用了ASSERT_EQ/EXPECT_EQ,正因它位于夹具类内部)。
注意ASSERT_EQ/ASSERT_TRUE与EXPECT_*的区别:ASSERT_*失败时立即终止当前测试(Dequeue中先ASSERT_TRUE(n != NULL)再解引用*n,避免空指针崩溃),而EXPECT_*失败仅记录错误并继续执行。
五、Sample #5:通过子夹具派生,让多个用例共享同一套前置/后置逻辑
一个夹具在 gtest 1.7 中只能被一个测试用例使用(TEST_F的第一个参数即夹具类名)。当多个测试用例需要相同或相似的前置/后置逻辑时(例如“所有测试都不得慢于 5 秒”“GUI 库测试不得泄漏字体/画刷等系统资源”),Sample #5 给出的模式是:把公共逻辑放进超类夹具(super fixture),再为每个用例派生专属子夹具。
定义超类夹具:QuickTest
class QuickTest : public testing::Test { protected: virtual void SetUp() { start_time_ = time(NULL); } virtual void TearDown() { const time_t end_time = time(NULL); EXPECT_TRUE(end_time - start_time_ <= 5) << "The test took too long."; } time_t start_time_; };超类夹具本身不绑定任何测试用例(没有名为QuickTest的用例,这是允许的)。它的TearDown()中直接使用断言检查运行时长——在 SetUp/TearDown 中同样可以使用断言,这是本例的亮点之一。
派生子夹具并逐级调用 SetUp
class IntegerFunctionTest : public QuickTest { // 无需额外逻辑,体为空 }; TEST_F(IntegerFunctionTest, Factorial) { EXPECT_EQ(1, Factorial(-5)); ... EXPECT_EQ(40320, Factorial(8)); } TEST_F(IntegerFunctionTest, IsPrime) { EXPECT_FALSE(IsPrime(-1)); ... EXPECT_TRUE(IsPrime(23)); }IntegerFunctionTest直接继承QuickTest,于是它内部的所有TEST_F测试自动获得“5 秒超时检查”。而另一个用例QueueTest需要额外的队列数据,则在自己的SetUp()中先显式调用超类SetUp(),再做附加初始化:
class QueueTest : public QuickTest { protected: virtual void SetUp() { QuickTest::SetUp(); // 先完成超类设置 q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); } // TearDown 默认继承 QuickTest::TearDown(),无需重写 Queue<int> q0_; Queue<int> q1_; Queue<int> q2_; };注释最后指出:夹具的派生层次在 gtest 中没有深度限制(可以从QueueTest再派生),但实践中不宜过深以免混乱。
六、Sample #6:类型参数化测试——让同一套测试跑遍所有实现
当需要验证同一接口的多个实现是否满足共同约束时(接口测试,interface tests),Sample #6 展示了两种做法。被测接口 prime_tables.h 提供PrimeTable抽象基类及其两个实现:OnTheFlyPrimeTable(实时计算素数)与PreCalculatedPrimeTable(预计算素数表)。
准备:夹具类模板与工厂函数
template <class T> PrimeTable* CreatePrimeTable(); template <> PrimeTable* CreatePrimeTable<OnTheFlyPrimeTable>() { return new OnTheFlyPrimeTable; } template <> PrimeTable* CreatePrimeTable<PreCalculatedPrimeTable>() { return new PreCalculatedPrimeTable(10000); } template <class T> class PrimeTableTest : public testing::Test { protected: PrimeTableTest() : table_(CreatePrimeTable<T>()) {} virtual ~PrimeTableTest() { delete table_; } PrimeTable* const table_; // 通过基类接口而非具体实现测试 };注意夹具通过基类指针持有被测对象,这更贴近真实使用场景,也避免了“实现类成员函数遮蔽基类同名函数”这类陷阱。
写法一:Typed Test(类型在编写时就已知)
#if GTEST_HAS_TYPED_TEST using testing::Types; typedef Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable> Implementations; TYPED_TEST_CASE(PrimeTableTest, Implementations); TYPED_TEST(PrimeTableTest, ReturnsFalseForNonPrimes) { EXPECT_FALSE(this->table_->IsPrime(-5)); EXPECT_FALSE(this->table_->IsPrime(0)); EXPECT_FALSE(this->table_->IsPrime(1)); EXPECT_FALSE(this->table_->IsPrime(4)); EXPECT_FALSE(this->table_->IsPrime(6)); EXPECT_FALSE(this->table_->IsPrime(100)); } TYPED_TEST(PrimeTableTest, ReturnsTrueForPrimes) { ... } TYPED_TEST(PrimeTableTest, CanGetNextPrime) { ... } #endif // GTEST_HAS_TYPED_TESTTYPED_TEST_CASE(夹具名, 类型列表)声明用例并指定类型参数;TYPED_TEST定义测试。gtest 会为类型列表中的每个类型各执行一遍所有TYPED_TEST,无需重复书写。模板世界中的两个注意事项:可用TypeParam指代当前类型参数,且访问夹具成员时必须显式写this->(C++ 模板两阶段查找的要求)。
写法二:Type-Parameterized Test(类型可在将来扩展)
当编写测试时还不知道全部待测类型(例如接口作者希望第三方日后实现接口并复用测试),则用_P后缀(P 代表 parameterized 或 pattern)的宏,将测试写成可注册、可实例化的“测试模式”:
#if GTEST_HAS_TYPED_TEST_P template <class T> class PrimeTableTest2 : public PrimeTableTest<T> {}; TYPED_TEST_CASE_P(PrimeTableTest2); TYPED_TEST_P(PrimeTableTest2, ReturnsFalseForNonPrimes) { ... } TYPED_TEST_P(PrimeTableTest2, ReturnsTrueForPrimes) { ... } TYPED_TEST_P(PrimeTableTest2, CanGetNextPrime) { ... } // 关键额外步骤:枚举已定义的测试 REGISTER_TYPED_TEST_CASE_P( PrimeTableTest2, ReturnsFalseForNonPrimes, ReturnsTrueForPrimes, CanGetNextPrime); // 实例化:指定实例名 + 用例名 + 类型列表 typedef Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable> PrimeTableImplementations; INSTANTIATE_TYPED_TEST_CASE_P(OnTheFlyAndPreCalculated, // 实例名 PrimeTableTest2, // 测试用例名 PrimeTableImplementations); // 类型列表 #endif // GTEST_HAS_TYPED_TEST_P相比 typed test,这种写法多出两步:必须用REGISTER_TYPED_TEST_CASE_P枚举测试,再用INSTANTIATE_TYPED_TEST_CASE_P绑定具体类型。好处是测试模式通常放在.h文件中,任何实现方#include后即可实例化,甚至可在同一程序中多次实例化,每个实例名会成为用例名的一部分、可用于测试过滤。
七、Sample #7:值参数化测试——参数是“值”而非“类型”
sample7_unittest.cc 演示值参数化测试:每个测试携带一个参数,该参数是被测实现的指针(此处是工厂函数指针)。与 Sample #6 的“类型参数”形成对照。
#if GTEST_HAS_PARAM_TEST using ::testing::TestWithParam; using ::testing::Values; typedef PrimeTable* CreatePrimeTableFunc(); PrimeTable* CreateOnTheFlyPrimeTable() { return new OnTheFlyPrimeTable(); } template <size_t max_precalculated> PrimeTable* CreatePreCalculatedPrimeTable() { return new PreCalculatedPrimeTable(max_precalculated); } class PrimeTableTest : public TestWithParam<CreatePrimeTableFunc*> { public: virtual ~PrimeTableTest() { delete table_; } virtual void SetUp() { table_ = (*GetParam())(); } virtual void TearDown() { delete table_; table_ = NULL; } protected: PrimeTable* table_; }; TEST_P(PrimeTableTest, ReturnsFalseForNonPrimes) { ... } TEST_P(PrimeTableTest, ReturnsTrueForPrimes) { ... } TEST_P(PrimeTableTest, CanGetNextPrime) { ... } INSTANTIATE_TEST_CASE_P( OnTheFlyAndPreCalculated, PrimeTableTest, Values(&CreateOnTheFlyPrimeTable, &CreatePreCalculatedPrimeTable<1000>)); #else TEST(DummyTest, ValueParameterizedTestsAreNotSupportedOnThisPlatform) {} #endif // GTEST_HAS_PARAM_TEST要点归纳:
- 夹具继承
TestWithParam<T>,在测试体/SetUp/TearDown中通过GetParam()获取当前参数值; - 测试用
TEST_P定义,参数化测试必须实例化才能运行:INSTANTIATE_TEST_CASE_P(实例名, 用例名, Values(...)),其中Values列出参数值集合;实例化可以在其他翻译单元完成,也可多次实例化; - 通用原则:为避免测试相互影响,被测对象应在每个测试中创建/销毁(本样例在
SetUp中创建、TearDown中删除)而非跨测试复用; - 平台兼容:当编译器不支持参数化测试(
GTEST_HAS_PARAM_TEST未定义)时,样例用条件编译输出一个DummyTest占位测试,以保持gtest_main被链接(否则 MSVC 会报 LNK1561 入口点缺失错误)。这种条件编译手法同样用于 Sample #6、#8。
八、Sample #8:Combine() 组合参数,穷举多标志组合
sample8_unittest.cc 解决“被测代码依赖多个全局标志变量”的测试场景:Combine()负责生成这些标志的全部可能组合,每个测试拿到一组组合作为参数。
样例构造了一个HybridPrimeTable(见 prime_tables.h):它同时持有OnTheFlyPrimeTable与PreCalculatedPrimeTable两个实现,根据阈值择优查询,且在低内存场景下可通过force_on_the_fly标志跳过预计算表的创建:
class HybridPrimeTable : public PrimeTable { public: HybridPrimeTable(bool force_on_the_fly, int max_precalculated) : on_the_fly_impl_(new OnTheFlyPrimeTable), precalc_impl_(force_on_the_fly ? NULL : new PreCalculatedPrimeTable(max_precalculated)), max_precalculated_(max_precalculated) {} ... virtual bool IsPrime(int n) const { if (precalc_impl_ != NULL && n < max_precalculated_) return precalc_impl_->IsPrime(n); else return on_the_fly_impl_->IsPrime(n); } ... };随后用Combine将Bool()(真假)与Range/Values类型的参数生成笛卡尔积:
#if GTEST_HAS_COMBINE INSTANTIATE_TEST_CASE_P( MeanAndMedian, HybridPrimeTableTest, Combine(Values(true, false), // force_on_the_fly Values(1, 10))); // max_precalculated #endif // GTEST_HAS_COMBINECombine()的价值在于:无需手工书写for循环嵌套,即可一次性覆盖多个布尔标志与取值范围的所有组合,特别适合验证“受多个开关影响”的类。该宏同样受GTEST_HAS_COMBINE条件编译保护。
九、Sample #9:Listener API 定制输出 + 反射 API 检查结果
前 8 个样例关注“怎么写测试”,Sample #9 转向“测试框架的可扩展性”。它演示两件事:用Listener API实现替代的控制台输出,用UnitTest 反射 API枚举用例与测试、检查其结果。
实现自定义监听器
class TersePrinter : public EmptyTestEventListener { private: virtual void OnTestProgramStart(const UnitTest& /* unit_test */) {} virtual void OnTestProgramEnd(const UnitTest& unit_test) { fprintf(stdout, "TEST %s\n", unit_test.Passed() ? "PASSED" : "FAILED"); fflush(stdout); } virtual void OnTestStart(const TestInfo& test_info) { fprintf(stdout, "*** Test %s.%s starting.\n", test_info.test_case_name(), test_info.name()); fflush(stdout); } virtual void OnTestPartResult(const TestPartResult& test_part_result) { fprintf(stdout, "%s in %s:%d\n%s\n", test_part_result.failed() ? "*** Failure" : "Success", test_part_result.file_name(), test_part_result.line_number(), test_part_result.summary()); fflush(stdout); } virtual void OnTestEnd(const TestInfo& test_info) { fprintf(stdout, "*** Test %s.%s ending.\n", test_info.test_case_name(), test_info.name()); fflush(stdout); } };EmptyTestEventListener是监听器接口的空实现基类,只需按需覆写事件方法即可,无需实现全部回调。可用的事件包括:OnTestProgramStart/End(整个测试程序开始/结束)、OnTestStart/End(单个测试开始/结束)、OnTestPartResult(每次断言产生结果时)。TestInfo反射对象提供test_case_name()、name()等方法;TestPartResult提供failed()、file_name()、line_number()、summary()等结果信息。
在 main 中装配监听器
int main(int argc, char **argv) { InitGoogleTest(&argc, argv); bool terse_output = false; if (argc > 1 && strcmp(argv[1], "--terse_output") == 0 ) terse_output = true; else printf("%s\n", "Run this program with --terse_output to change the way " "it prints its output."); UnitTest& unit_test = *UnitTest::GetInstance(); if (terse_output) { // 移除默认监听器,注册 TersePrinter TestEventListeners& listeners = unit_test.listeners(); delete listeners.Release(listeners.default_result_printer()); listeners.Append(new TersePrinter); } return RUN_ALL_TESTS(); }核心手法是:通过UnitTest::GetInstance()拿到全局单例,listeners.Release(...)移除默认结果打印器,listeners.Append(new TersePrinter)挂载自定义监听器。同时借助反射 API(UnitTest、TestCase、TestInfo等)枚举并检查结果。运行时用--terse_output开关切换输出模式。样例附带三个演示测试(PrintsMessage、Succeeds、Fails),其中Fails故意EXPECT_EQ(1, 2)失败,用于演示替代格式的失败信息。
十、Sample #10:用 Listener API 打造简易内存泄漏检查器
Sample #10 把 Listener API 应用到实际工程问题——内存泄漏检测。思路朴素而有效:统计每个测试运行前后某类对象的存活数量,若测试结束后数量反而增加,即判定泄漏。
可统计分配次数的 Water 类
class Water { public: void* operator new(size_t allocation_size) { allocated_++; return malloc(allocation_size); } void operator delete(void* block, size_t /* allocation_size */) { allocated_--; free(block); } static int allocated() { return allocated_; } private: static int allocated_; }; int Water::allocated_ = 0;通过重载operator new/operator delete维护静态计数器allocated_,即可知道任意时刻存活的Water对象数量。
监听器实现泄漏检查
class LeakChecker : public EmptyTestEventListener { private: virtual void OnTestStart(const TestInfo& /* test_info */) { initially_allocated_ = Water::allocated(); } virtual void OnTestEnd(const TestInfo& /* test_info */) { int difference = Water::allocated() - initially_allocated_; EXPECT_LE(difference, 0) << "Leaked " << difference << " unit(s) of Water!"; } int initially_allocated_; };LeakChecker在OnTestStart记录初始存活数,在OnTestEnd用EXPECT_LE断言增量不大于 0。源码注释特别提醒:除OnTestPartResult外,任何事件处理器中都可以使用 gtest 断言产生失败。
演示效果
TEST(ListenersTest, DoesNotLeak) { Water* water = new Water; delete water; } // 当指定 --check_for_leaks 时,此测试应当失败 TEST(ListenersTest, LeaksWater) { Water* water = new Water; EXPECT_TRUE(water != NULL); }main()中同样通过--check_for_leaks命令行开关决定是否装配LeakChecker。运行后,LeaksWater测试(漏掉了delete)会因增量大于 0 而失败并报告“Leaked 1 unit(s) of Water!”,DoesNotLeak则正常通过。这个不足百行的原型演示了如何以极低成本为工程引入基础的内存泄漏监控。
十一、本地运行与源码导航
所有样例的编译入口可参考仓库中的构建配置:gtest 自身通过 BUILD.gn(GN 构建)与 CMakeLists.txt(CMake 构建)组织,样例目录 samples 亦含配套构建描述。在支持 CMake 的环境中,可配置 gtest 构建并启用gtest_build_samples选项,然后运行对应样例可执行文件;Sample #9 与 Sample #10 分别支持--terse_output、--check_for_leaks命令行开关,用于演示监听器的实际效果。
进一步深入研究可参考以下仓库文件:
- 框架核心声明:include/gtest/gtest.h(全部断言宏与
TEST/TEST_F宏定义) - 测试入口实现:src/gtest_main.cc(
RUN_ALL_TESTS的调用入口) - 事件监听接口:include/gtest/gtest.h 中
TestEventListener/EmptyTestEventListener定义 - 被测样例目标:samples/prime_tables.h、samples/sample1.cc、samples/sample2.cc、samples/sample3-inl.h、samples/sample4.cc
结语:从 10 个样例建立 gtest 的完整心智模型
回看这 10 个样例,它们几乎覆盖了 gtest 1.7 的全部核心机制:TEST定义测试、EXPECT_*/ASSERT_*断言、测试夹具与TEST_F、夹具继承复用、typed / type-parameterized / value-parameterized 三种参数化测试、Combine()组合参数,以及通过 Listener API 定制输出与检测内存泄漏。其中蕴含的两条设计原则值得贯穿始终:测试必须相互独立(夹具按测试重建、断言不依赖执行顺序),以及优先使用失败信息更丰富的断言(EXPECT_EQ优于裸EXPECT_TRUE)。无论是为 miniblink49 这类大型 C++ 工程补充回归测试,还是在自己的项目引入单元测试体系,这套样例都是最佳起点——官方文档 V1_7_Samples.md 则始终是定位这些样例的快捷索引。
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考