简介:gtest源码包包含Google Test框架的完整源码,面向C++开发者、测试框架研究者和有志于提升单元测试能力的工程师,可用于剖析框架内部机制、改进自身测试实践。压缩包共180个文件、约1.07MB,主体为70个cc实现文件与31个h头文件,另有Python脚本、m4宏、Visual Studio工程文件等,分别承担源码生成、构建配置与跨平台工程支持,目录结构清晰便于定位。源码覆盖测试用例与测试点管理、丰富的断言体系、TEST_P参数化测试、命令行过滤、XML测试报告输出等模块;通过TEST_F宏定义测试点,EXPECT_EQ、EXPECT_THROW等断言可分别处理数值比较与异常场景,INSTANTIATE_TEST_SUITE_P则实现了参数化复用。从main入口到UnitTest单例调度,再到EXPECT_*/ASSERT_*等断言宏的实现,都展示了Google C++测试框架的优雅设计。目前已有522人学习,适合有一定C++基础、希望深入掌握gtest运行原理或准备二次开发的读者,反复研读可加深对测试驱动开发(TDD)与行为驱动开发(BDD)理念的理解。
1. 读 gtest 源码,先认路:目录、核心文件与版本差异
1.1 源码目录怎么看:三个目录、两个头文件
拿到 googletest 源码后,很多人第一反应是找个 README 从头读,其实没必要。gtest 源码的正门在 googletest 目录下,核心结构非常清晰,我平时只重点盯几个文件。
include/gtest/gtest.h:对外主头文件,断言宏、TEST 系列宏、Test 基类、TestInfo、UnitTest 的声明全在这。include/gtest/gtest-param-test.h:参数化测试相关宏和类模板,TEST_P、INSTANTIATE_TEST_SUITE_P 的声明。include/gtest/internal/gtest-port.h:平台适配层,线程、原子操作、文件路径、系统判断全在这层抽象。include/gtest/internal/gtest-internal.h:内部注册函数、宏辅助结构,MakeAndRegisterTestInfo 的声明也在这里。src/gtest.cc:最核心的实现文件,UnitTest、TestSuite、TestInfo、TestResult 的实现基本都在里面。src/gtest-internal-inl.h:内部实现头文件,UnitTestImpl、TestEventRepeater、默认监听器都在这里。
读 gtest 源码最容易犯的错,是想把每个文件从头看到尾。gtest 有大量处理极端编译环境的宏,比如老编译器、无 RTTI、异常关闭等情况,这些对理解测试框架本身帮助不大。第一遍读,我建议只看gtest.h和gtest.cc这条主链路,先把测试从“注册”到“执行”到“输出结果”的完整流程串起来。
1.2 版本选型与阅读入口
gtest 的源码版本差异很大,尤其是 1.10 之后,把TestCase改成了TestSuite,相关内部类名也换了。网上不少文章写的是 1.8 时代的代码,如果你照搬去看新版源码,会对着TestCase找半天,最后发现类名早就不存在了。
我建议直接锁定一个稳定 tag 来读。目前源码中INSTANTIATE_TEST_CASE_P已经废弃,正式名称是INSTANTIATE_TEST_SUITE_P;内部类从TestCase全部更名为TestSuite。如果你用git clone拉的是最新主干,里面还会夹杂新的 API 演进代码,新手容易看晕。按我的习惯,直接git checkout v1.14.0,再开始读。
找阅读入口也有技巧。源码中真正的主线是RUN_ALL_TESTS(),这是一个宏,展开后是UnitTest::GetInstance()->Run()。顺着Run()往下走,你就能看到整个测试执行的生命周期。这条线理清楚后,其他如断言机制、事件监听器、参数化测试,都是挂在这条主线上的一节节车厢。
1.3 gtest.h 到 gtest.cc 的主调用线
用最小的例子来说,一个空测试程序最终会经历这些步骤:
main()调用RUN_ALL_TESTS()。- 宏展开成
UnitTest::GetInstance()->Run()。 UnitTest::Run()内部拿到UnitTestImpl,遍历所有TestSuite。- 每个
TestSuite遍历自己名下的TestInfo。 - 每个
TestInfo调用内部工厂对象创建测试 fixture 实例。 - 执行
SetUp()、TestBody()、TearDown()。 - 测试结果写入
TestResult。 - 监听器接收到各种事件回调,决定如何输出内容。
这一整套设计,本质上是一个“注册中心 + 工厂 + 事件回调”的组合。你在gtest.cc里能看到Run()的实现基本就是这个骨架。有了这条主调用线,后面读 TEST 宏、断言、监听器时,你会不停地发现:原来这些看似独立的功能,最后都落回这条线上。
2. TEST 宏展开:测试用例是怎么“凭空”冒出来的
2.1 TEST 宏到底把测试函数藏在了哪
很多人第一次好奇 gtest 源码,都是因为同一个问题:我只写了TEST(MySuite, MyTest) { ... },没有注册,没有调用,测试怎么就被框架发现了?
答案在宏里。TEST宏一层层展开,最终会展开成一个类和一个静态变量。把这段逻辑简化后,大致长这样:
#define TEST(test_suite_name, test_name) \ class test_suite_name##_##test_name##_Test : public ::testing::Test { \ public: \ test_suite_name##_##test_name##_Test() = default; \ private: \ void TestBody() override; \ }; \ class test_suite_name##_##test_name##_Test##_Register { ... }; \ void test_suite_name##_##test_name##_Test::TestBody()也就是说,你写的TEST(MySuite, MyTest) { ... }本质上定义了一个名为MySuite_MyTest_Test的类,而大括号里那坨代码,其实是这个类的成员函数TestBody()的函数体。
这个技巧非常巧妙。用户不需要知道类的存在,只要在宏后面直接跟一个函数体,宏就帮你把函数体“塞进”了继承自Test的派生类里。你写的是函数体,编译器的视角里却是一个完整的类定义加成员函数实现。
2.2 MakeAndRegisterTestInfo 与静态注册
光有类还不能被框架发现。真正的注册动作,来自类外部的静态变量初始化。源码中生成的代码大概是:
::testing::TestInfo* const test_info_ = \ ::testing::internal::MakeAndRegisterTestInfo( "MySuite", "MyTest", nullptr, nullptr, ::testing::internal::CodeLocation(__FILE__, __LINE__), ::testing::internal::GetTestTypeId(), ::testing::internal::SuiteApiResolver<MySuite_MyTest_Test>::GetSetUpTearDownSuite(), new ::testing::internal::TestFactoryImpl<MySuite_MyTest_Test>);注意,这里定义的是全局静态变量,初始化发生在main()之前。所以测试用例的注册,不需要任何显式调用,只要测试程序一启动,所有 TEST 宏生成的注册代码就已经全部执行完。
MakeAndRegisterTestInfo内部会找到全局唯一的UnitTestImpl,在里面按测试套件名分组,建好TestSuite和TestInfo对象,并把工厂对象保存下来。这个工厂对象非常关键,它负责后续真正创建测试实例,相当于一个延迟到运行期才生效的“构造说明书”。
明白了这一点,你也就理解了为什么 gtest 的测试用例数量和顺序在运行时是确定的。它们不是动态发现的,而是在程序启动阶段,由静态初始化顺序决定的。
2.3 RunTest 里 SetUp、TestBody、TearDown 的调用真相
注册完成后,真正执行测试的逻辑在gtest.cc的TestInfo::Run()里。这里做的事情可以概括为:用之前保存的工厂对象创建测试实例,然后调用三个关键方法。
简化后的执行逻辑是:
- 调用
test->SetUp()。 - 调用
test->TestBody(),也就是你写的测试逻辑。 - 调用
test->TearDown()。
这三个调用被包在异常处理中。如果TestBody()抛出异常,gtest 会捕获并将异常信息记录到TestResult里,测试程序不会直接崩溃。这一点在设计测试框架时很值得学习:测试代码出问题,框架本身不能跟着崩,要把失败信息收集起来统一输出。
源码里还有一个细节:SetUp()和TearDown()的执行顺序是由Test::Run()控制的,不是由用户控制的。所以无论你写多少个 TEST_F,框架都能保证每个测试都独立执行一遍 SetUp/TearDown,这正是 fixture 测试语义的基础。
3. ASSERT 与 EXPECT 的分岔路:返回值、return 与失败记录
3.1 断言宏的通用外壳
断言是 gtest 使用频率最高的功能,而ASSERT_*和EXPECT_*的核心区别,很多人只是背结论:前者失败就终止当前测试,后者失败继续跑。但要真正理解,必须看宏的实现。
以ASSERT_TRUE(condition)为例,它最终展开为类似这样的结构:
if (const ::testing::AssertionResult gtest_ar_ = AssertionResult(condition)) { // 空分支 } else { return GTEST_MESSAGE_(...); // 实际是一段记录失败并 return 的逻辑 }这里有两个关键设计。第一,用if (const AssertionResult gtest_ar_ = ...)把条件的真假和失败信息的构造绑定在一起,后续失败处理可以直接读取这个对象里的详细内容。第二,两个分支之间不需要用户写 else,宏已经替你安排好了。
源码中还有GTEST_AMBIGUOUS_ELSE_BLOCKER_这个宏,它用来避免用户写if (...) ASSERT_TRUE(...); else ...时出现的悬挂 else 问题。这种小细节,如果你只是使用断言,一辈子感知不到,但把它们读了,你会对宏设计的边界情况更敏感。
3.2 为什么 ASSERT 能“中断”测试而 EXPECT 不能
ASSERT_*的失败分支里有return,而EXPECT_*的失败分支里没有。一句话讲完,但值得再往深挖一点。
在 gtest 源码中,ASSERT_*系列宏的失败处理是GTEST_FATAL_FAILURE_,它展开后包含return;。因此,只要断言失败,控制流会立刻从当前测试函数中返回。EXPECT_*系列走的是GTEST_NONFATAL_FAILURE_,它只负责构造一条TestPartResult并推入当前测试的结果列表,随后继续执行后续代码。
所以,从控制流层面看,ASSERT_*本质上是一个“带 return 的 if 判断”,并不是什么特殊的异常机制。理解了这一点,你就能推断出很多行为。比如在循环里使用ASSERT_*,失败时退出的是整个测试函数,而不是仅跳出循环;如果你希望失败后继续检查其余循环项,就得用EXPECT_*。
3.3 在非 void 辅助函数里用 ASSERT 为什么编译失败
这是个很经典的坑。你写了一个辅助函数,希望在里面用ASSERT_TRUE做前置校验,结果编译直接报错,错误信息里还带着return;之类的字样。
原因正是上面说的:ASSERT_*的失败分支是return;,而 C++ 不允许在一个非 void 返回类型的函数里执行裸return;。换句话说,ASSERT_*只能在返回类型为void的函数里使用。
这个限制不算不合理,但确实让不少人在封装公共校验逻辑时头疼。我的经验是:如果辅助函数就是为测试服务的,直接把它声明成void,内部用ASSERT_*即可。如果你确实需要一个返回值的辅助函数,又要在内部做断言,那只能用EXPECT_*或返回AssertionResult自己拼。
每次在源码里看到这种设计,我都会提醒自己:宏不是函数,它展开后是写在调用处的代码。理解宏展开后的代码长什么样,很多“怪问题”其实都不怪。
4. 事件监听器:控制输出、扩展框架的钩子
4.1 TestEventListener 的方法时机
用到 gtest 的人一般都知道测试失败会打印一堆信息,但很少有人关心这些信息是从哪来的。答案就是事件监听器机制。TestEventListener是 gtest 对测试过程所有关键节点的回调抽象。
监听器的虚方法覆盖了测试完整生命周期:
OnTestProgramStart:整个测试程序开始时触发。OnTestSuiteStart:一个测试套件开始时触发。OnTestStart:单个测试用例开始时触发。OnTestPartResult:每次断言产生结果时触发,这是最常用的事件。OnTestEnd:单个测试用例结束时触发。OnTestSuiteEnd、OnTestProgramEnd:对应结束阶段。
这些回调的调用点散布在UnitTest::Run()、TestSuite::Run()、TestInfo::Run()里。读完源码你会发现,测试执行的过程本质上是一连串事件的分发过程,具体业务动作都是挂在事件处理器上的。这种设计解耦性很好,想扩展行为时不需要改动核心执行逻辑。
4.2 默认监听器与替换策略
gtest 在程序启动时会默认创建两个监听器并挂到UnitTest::GetInstance()->listeners()上。一个是DefaultPrinter,负责把测试结果打印到标准输出;另一个是XmlUnitTestPrinter,负责生成 XML 格式的测试报告,供 CI 系统解析。
如果你只是想额外加点东西,用listeners().Append()追加一个监听器即可。但要注意,默认打印器还在,你追加的内容是叠加上去的。如果你想完全自定义输出格式,不想要默认那套终端打印,需要先把默认监听器移除。
移除方法不复杂但容易踩坑:
testing::UnitTest::GetInstance()->listeners().Release( testing::UnitTest::GetInstance()->listeners().default_result_printer());default_result_printer()返回的是默认监听器的指针,Release()会把它从列表中摘下但不 delete,这个对象由 gtest 自己管理,你不用理会。经常有人直接delete这个指针,导致后续崩掉,这是误操作。
4.3 一个自定义 JSON 输出监听器的实际写法
理解了监听器机制后,扩展起来其实很直接。比如我想输出一份 JSON 格式的结果,只需要继承TestEventListener,把关心的几个事件重写掉。
class JsonListener : public testing::TestEventListener { public: void OnTestProgramStart(const testing::UnitTest&) override {} void OnTestIterationStart(const testing::UnitTest&, int) override {} void OnEnvironmentsSetUpStart(const testing::UnitTest&) override {} void OnEnvironmentsSetUpEnd(const testing::UnitTest&) override {} void OnEnvironmentsTearDownStart(const testing::UnitTest&) override {} void OnEnvironmentsTearDownEnd(const testing::UnitTest&) override {} void OnTestStart(const testing::TestInfo& test_info) override {} void OnTestPartResult(const testing::TestPartResult& result) override { if (result.failed()) { printf("{\"file\":\"%s\",\"line\":%d,\"message\":\"%s\"}\n", result.file_name(), result.line_number(), result.message()); } } void OnTestEnd(const testing::TestInfo& test_info) override { if (test_info.result()->Failed()) { printf("{\"test\":\"%s.%s\",\"status\":\"failed\"}\n", test_info.test_suite_name(), test_info.name()); } } void OnTestSuiteStart(const testing::TestSuite&) override {} void OnTestSuiteEnd(const testing::TestSuite&) override {} void OnTestProgramEnd(const testing::UnitTest&) override {} };这里有一个我自己踩过的坑:OnTestPartResult的输出最好不要用std::cout,而用printf直接写标准输出。因为 gtest 默认监听器在输出测试进度时会带上自己的格式,混杂std::cout的流式缓存可能打乱顺序。再者,如果你在监听器里再调用断言,会触发递归事件,那是灾难。监听器里只做记录和转发,绝不要触发新断言。
5. 参数化测试:从 INSTANTIATE 宏看实例注册流程
5.1 TestWithParam 与 GetParam 的类型桥
参数化测试源码的入口是include/gtest/gtest-param-test.h。TEST_P生成的测试类,父类是TestWithParam<T>,而这个类的全名其实是TestWithParam,它继承自Test和WithParamInterface<T>。
WithParamInterface<T>提供了GetParam()方法。它的内部实现不复杂:参数值保存在一个ParamGenerator<T>或者类似std::tuple的容器里,框架在运行前会把参数注入到测试实例中。所以测试执行时,GetParam()返回的是框架注入的那个值,而不是从全局变量里取。
理解这个类型桥很重要。源码角度来说,TEST_P和TEST的区别在于:TEST_P生成的类是一个模板工厂,它不知道具体参数值,参数值是后来通过INSTANTIATE_TEST_SUITE_P提供的。也就是说,TEST_P只定义了“怎么测”,INSTANTIATE才定义了“测哪些”。
5.2 INSTANTIATE_TEST_SUITE_P 的注册链
INSTANTIATE_TEST_SUITE_P的宏展开比TEST_P更复杂,核心逻辑是创建一个匿名静态对象,这个对象的构造函数里会迭代你传进来的参数序列,并为每个参数值调用注册函数,生成一个带编号的测试实例。
大致流程是:
- 宏展开生成一个静态实例,类型是实现参数化注册的内部类。
- 该实例的构造函数调用参数生成器,得到所有参数值。
- 对每个参数值,调用
ParameterizedTestSuiteRegistry的注册函数。 - 注册时会把
TEST_P定义的那个测试模板实例化出具体版本,并分配一个名字。
比如你写了INSTANTIATE_TEST_SUITE_P(Nums, MyTest, Values(1, 2, 3)),最终框架里会出现三个测试实例,名字类似Nums/MyTest/0、Nums/MyTest/1、Nums/MyTest/2。那个序号其实就是参数序号。
如果想给参数起可读的名字,可以用Values("a", "b")配合能打印的类型,或者直接重载打印函数。参数名生成依赖operator<<或打印函数,如果你的参数类型没有流输出实现,就会退化成纯编号。
5.3 参数名与“未实例化”报错
实际开发中,参数化测试最常见的编译错误是undefined reference to '... INSTANTIATE_TEST_SUITE_P ...'或者运行时提示“没有注册任何测试”。这类问题直接看源码就能明白原因:TEST_P只生成了一个模板化的注册入口,真正把测试推进注册表的动作发生在INSTANTIATE_TEST_SUITE_P宏生成的静态对象构造中。你忘了写INSTANTIATE_TEST_SUITE_P,那这个静态对象就不存在,测试自然没注册。
还有一类问题是参数类型不支持打印时报错。gtest 需要为参数生成名称,如果类型没有可用的流输出操作符,代码就无法通过编译。解决办法是给类型实现operator<<,或者在TEST_P内部完全不依赖默认打印。
顺手提一句,老代码里经常见到INSTANTIATE_TEST_CASE_P,这是旧版命名。新版源码虽然还保留兼容宏,但内部已经完全迁到SUITE后缀。见到TestCase和TestSuite混用不用慌,记住Suite是新的就行。
6. 查源码时的几个实用技巧
6.1 用预处理展开宏看真身
读 gtest 源码,最难啃的是宏。宏不像函数,你点一下跳转就能看到完整实现,它可能嵌套了七八层。我的做法是直接让编译器把宏展开,看展开后的真实代码。
在命令行对测试文件做预处理,并把结果里带_Test的类名打印出来:
g++ -E test.cc | grep -A 30 "MySuite_MyTest_Test"这样你能直接看到TEST宏到底生成了哪些类、哪些变量、哪些注册代码。比在源码里层层跳转更直白。我第一次用这招时,被展开出来的那一大坨代码震慑住了,但也正是那一刻才真正理解了“宏就是文本替换”这件事有多暴力。
如果你在 IDE 里开发,也可以右键宏选择“展开宏”,效果类似。这个技巧不仅适用于 gtest,任何重度使用宏的库都适用。
6.2 在注册点打断点看调用栈
读源码容易有一个问题:光看代码知道会注册,但没感知它什么时候发生。最好的办法是直接在MakeAndRegisterTestInfo打一个断点,然后启动任意测试程序。
你会发现,程序还没进main(),断点就命中了几十次或上百次。每命中一次,调用栈里都能看到一串静态初始化的过程。这会给你极大的冲击感:原来测试用例在main()之前就已经全部排队等着了。
顺着这个断点往调用栈上层看,能看到具体是哪个源文件的哪个 TEST 宏在注册。配合栈回溯逐个展开,你会对整个“静态注册”机制有非常直观的体感。这是光读源码很难获得的认知。
6.3 源码更新后的坑
最后说一个经验。gtest 的源码迭代并不慢,内部类名、宏名都变过。如果你在阅读中遇到某段代码报错或者找不到定义,先别怀疑自己,先检查版本。
我遇到过好几次这样的情况:网上教程里写的是testing::internal::UnitTestImpl,我手上这个版本改成了别的名字;或者某内部函数从src/gtest.cc挪到了gtest-internal-inl.h。这类问题在源码阅读中非常常见,反而是正常的。
读源码时不要试图理解每一个宏。gtest 处理老旧编译器的兼容宏、平台差异宏非常杂,这些跟测试框架的思想关系不大。把UnitTest、TestSuite、TestInfo、TestResult、监听器这条主干搞清楚,就已经超过了绝大多数使用者。后续真要魔改或者移植,再对照具体场景去查分支代码也不迟。
另外提醒一句,如果想把 gtest 源码移植到资源受限的环境,要注意gtest-port.h里的平台判断和控制宏,很多平台相关代码集中在这里。但这是另一个话题了,我这边暂时不展开。
本文还有配套的精品资源,点击获取