miniblink49 内置 V8 6.7 Google Test 样例指南:从 TEST 到 Listener API 的 10 个官方示例详解
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
本文基于 miniblink49 仓库中v8_6_7/testing/gtest/docs/V1_7_Samples.md这份 Google Test 样例索引文档展开,逐一解读 samples 目录 下的 10 个官方示例源文件,并结合 README.md 中的构建说明,讲清如何用 CMake 或 Makefile 编译并运行这些样例。读完后你能掌握 Google Test 的完整核心能力:TEST/TEST_F基本用法、测试夹具(test fixture)与超夹具继承、类型化测试、值参数化测试与Combine(),以及用 Listener API 定制输出和实现简易内存泄漏检查器。
背景:miniblink49 中的 Google Test 在哪里
miniblink49 是一个轻量 Blink 内核,仓库中按版本内嵌了多套 V8(v8_4_5到v8_7_5)。其中 v8_6_7/testing/gtest 目录内置了 Google Test 框架的完整源码,包括头文件(include)、实现(src)、文档(docs)和样例(samples)。从 docs 目录 的命名(V1_7_Samples.md、V1_7_Primer.md等)可以推断,这里内置的是 gtest 1.7 一代的 API 与文档,本文所有示例代码都以这一版本为准。
V1_7_Samples.md 本身是一份样例索引:它指出 samples 目录中有大量注释详尽的示例,覆盖 Google Test 的各种特性,并列出 Sample #1 到 #10 各自演示的功能。下表完整继承该索引,并把每个条目映射到仓库中的实际文件:
| 编号 | 文件 | 演示内容 |
|---|---|---|
| #1 | sample1_unittest.cc | 用 Google Test 测试 C++ 函数的基本步骤 |
| #2 | sample2_unittest.cc | 对含多个成员函数的类做较复杂的单元测试 |
| #3 | sample3_unittest.cc | 使用测试夹具(test fixture) |
| #4 | sample4_unittest.cc | 又一个使用 Google Test 的基础示例 |
| #5 | sample5_unittest.cc | 通过派生子夹具让多个测试用例复用同一夹具 |
| #6 | sample6_unittest.cc | 类型参数化测试(type-parameterized tests) |
| #7 | sample7_unittest.cc | 值参数化测试(value-parameterized tests)入门 |
| #8 | sample8_unittest.cc | 在值参数化测试中使用Combine() |
| #9 | sample9_unittest.cc | 用 Listener API 定制控制台输出,并用反射 API 检查测试结果 |
| #10 | sample10_unittest.cc | 用 Listener API 实现一个简易内存泄漏检查器 |
这些样例依赖被测代码头文件:sample1.h(Factorial/IsPrime)、sample2.h(MyString)、sample3-inl.h(Queue)、sample4.h(Counter)和 prime_tables.h(素数表接口及其两种实现)。
构建与运行样例
README.md 给出了完整构建流程,三种方式任选其一,均可用来编译并运行上述样例。
方式一:手动 g++ 编译。设 gtest 所在目录为${GTEST_DIR}(在本仓库即v8_6_7/testing/gtest),编译框架本体:
g++ -isystem ${GTEST_DIR}/include -I${GTEST_DIR} \ -pthread -c ${GTEST_DIR}/src/gtest-all.cc ar -rv libgtest.a gtest-all.o(-pthread是必需的,因为 Google Test 使用线程。)再编译你自己的测试文件并与libgtest.a链接:
g++ -isystem ${GTEST_DIR}/include -pthread path/to/your_test.cc libgtest.a \ -o your_test方式二:make/ 目录下的 Makefile。make/ 提供一个可用 GNU make 的 Makefile,它只构建 Google Test 库和一个样例测试。按 README 说明:
cd ${GTEST_DIR}/make make ./sample1_unittest如果环境不同导致出错,README 建议在make/Makefile中按其内嵌说明调整变量。
方式三:CMake(推荐,可编译全部样例)。gtest 自带 CMakeLists.txt,典型流程:
mkdir mybuild cd mybuild cmake ${GTEST_DIR}如果还想构建 Google Test 的样例(即本文讲解的 10 个 sample),需要打开gtest_build_samples选项:
cmake -Dgtest_build_samples=ON ${GTEST_DIR}在 *nix 下随后执行make即可;Windows + Visual Studio 会生成gtest.sln和.vcproj工程,Mac OS X + Xcode 会生成.xcodeproj。README 同时提到,Visual Studio 用户也可以直接使用msvc/目录下的gtest.sln/gtest-md.sln(-md后缀对应动态运行时 /MD,无后缀对应静态运行时 /MT;注意 gtest 与测试代码必须用同一种运行时选项编译),不过这些遗留工程已不再积极维护,README 更推荐 CMake 或集成到自有构建系统。
Sample #1:用 TEST 宏测试自由函数的“1-2-3”步骤
sample1_unittest.cc 是入门样例,其文件头注释把写单元测试概括为三步:
- 包含头文件:
#include <limits.h>、被测代码头sample1.h,以及声明测试框架的gtest/gtest.h(见 L46-L48)。 - 用
TEST宏定义测试:TEST(用例名, 测试名)后接花括号内的测试逻辑。 - 在 main() 中调用
RUN_ALL_TESTS():本样例没有手写 main,而是链接src/gtest_main.cc,由其中的 main 调用RUN_ALL_TESTS(),运行所有测试、打印结果,成功返回 0,否则返回 1(见 L143-L153)。
测试体本身非常直观,例如测试Factorial()的负数输入:
TEST(FactorialTest, Negative) { EXPECT_EQ(1, Factorial(-5)); EXPECT_EQ(1, Factorial(-1)); EXPECT_GT(Factorial(-10), 0); }(L79-L100)
样例中的<TechnicalDetails>注释补充了几个关键知识点:
- 测试按测试用例(test case)分组,逻辑相关的测试放进同一用例;用例名和测试名都必须是合法 C++ 标识符,且不能包含下划线。
EXPECT_EQ(expected, actual)等价于EXPECT_TRUE((expected) == (actual)),但断言失败时会打印期望值和实际值,因此优先使用EXPECT_EQ这类比较宏;EXPECT_TRUE接受任意布尔表达式,更通用。- Google Test 保证每个定义的测试恰好运行一次,但不保证执行顺序,因此测试之间不能依赖顺序、不能相互影响。
Sample #2:测试一个多成员函数的类
sample2_unittest.cc 测试一个简单的字符串类MyString。文件头注释给出组织建议:通常一个方法对应一个测试(不强制,但有助于保持测试有序),必要时再补充额外测试。四个测试分别覆盖:
- 默认构造函数(L49-L75);
- 从 C 字符串构造(L80-L85);
- 拷贝构造(L88-L92);
Set()方法,包括“把输入指针设为对象自身已持有的指针”和“设为 NULL”这类边界场景(L95-L109)。
其中默认构造测试里有一条值得注意的写法:
EXPECT_STREQ(NULL, s.c_string());注释解释了为什么不用EXPECT_EQ:EXPECT_EQ需要知道参数类型以便失败时打印值,而NULL被#define为0,编译器会选用 int 的格式化函数,与 gcc 3.4 的指针语义检查冲突而告警。根因是 C++ 未区分整数 0 和空指针常量,字符串比较场景用EXPECT_STREQ即可绕开(L52-L72)。
Sample #3:测试夹具 TEST_F
sample3_unittest.cc 引入测试夹具:夹具是所有测试共享的公共对象和函数,避免在每个测试里重复初始化/清理代码,也适合放置测试需要频繁调用的子例程。核心写法:从testing::Test派生夹具类,用TEST_F(夹具名, 测试名)定义测试:
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) { /* 用断言校验 q->Map(Double) */ } Queue<int> q0_; Queue<int> q1_; Queue<int> q2_; }; TEST_F(QueueTest, Dequeue) { int * n = q0_.Dequeue(); EXPECT_TRUE(n == NULL); // ... }(L70-L144)
<TechnicalDetails>部分阐明了三条设计原则(L44-L64):
- 测试共享夹具是代码共享而非数据共享——每个测试拿到的都是夹具的一份全新拷贝,一个测试修改的数据不会传递给下一个测试;这样保证测试独立、可重复。
SetUp()在每个测试运行前调用,TearDown()在每个测试运行后调用,无初始化/清理需求时可省略。EXPECT_TRUE、FAIL等断言宏内部需要知道“当前测试”是谁(打印结果时要标明失败属于哪个测试),技术上它们调用Test类的成员函数,因此不能在全局函数里使用断言——测试子例程必须放在夹具内。样例里的MapTester()就演示了这一点:它作为夹具成员使用ASSERT_EQ和EXPECT_EQ(L96-L111)。
Sample #4:断言参数的求值语义
sample4_unittest.cc 很短,测试Counter::Increment():
TEST(Counter, Increment) { Counter c; // EXPECT_EQ() evaluates its arguments exactly once, so they // can have side effects. EXPECT_EQ(0, c.Increment()); EXPECT_EQ(1, c.Increment()); EXPECT_EQ(2, c.Increment()); }(L36-L45)
这个样例的教学点很具体:EXPECT_EQ()对参数恰好求值一次,因此参数可以带副作用——这里连续三次Increment()的自增副作用是安全的,如果断言宏多次求值,返回值就会错乱。
Sample #5:超夹具——让多个用例复用一套前置逻辑
sample5_unittest.cc 解决“多个测试用例想用相同(或略有不同)夹具”的问题。约束在于:定义夹具时同时指定了使用它的测试用例名,一个夹具只能服务一个用例。解法是把共享逻辑放进一个超夹具(super fixture),再让各用例的夹具从它派生。
样例的目标是“任何测试超过 5 秒就算失败”。超夹具QuickTest在SetUp()记录开始时间,在TearDown()断言耗时:
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_; };(L63-L85)
两个要点:其一,QuickTest本身没有对应的测试用例,这完全合法——它只是给别人继承的基类;其二,断言宏在SetUp()/TearDown()中同样可用。随后IntegerFunctionTest(空派生,直接继承“快速”约束)与QueueTest(在SetUp()里先调QuickTest::SetUp()再初始化队列,L144-L167)各自从QuickTest派生,两个用例的测试就自动受 5 秒时限约束;TearDown()默认继承QuickTest::TearDown(),无额外清理工作时可省略。文件末尾还说明:可以从派生夹具继续派生更深层的夹具,Google Test 不限制层级深度,但实践中不宜过深以免难以理解(L195-L199)。
Sample #6:类型化测试与类型参数化测试
sample6_unittest.cc 演示“接口测试”:用同一套测试验证同一接口的多个实现(这里是被 prime_tables.h 声明的PrimeTable接口的两种实现OnTheFlyPrimeTable与PreCalculatedPrimeTable)。Google Test 提供两种复用方式:
类型化测试(typed tests)——写测试时已知全部被测类型。先定义工厂函数和夹具类模板(L44-L75):
template <class T> PrimeTable* CreatePrimeTable(); template <> PrimeTable* CreatePrimeTable<OnTheFlyPrimeTable>() { return new OnTheFlyPrimeTable; } template <class T> class PrimeTableTest : public testing::Test { protected: PrimeTableTest() : table_(CreatePrimeTable<T>()) {} virtual ~PrimeTableTest() { delete table_; } PrimeTable* const table_; };夹具成员table_声明为基类接口指针而非具体实现类型,注释说明这是关键:测试应通过基类接口访问实现,贴近真实调用场景,避免实现类中同名但签名不同的方法“遮蔽”基类方法导致的坑。然后声明用例并定义类型化测试:
typedef Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable> Implementations; TYPED_TEST_CASE(PrimeTableTest, Implementations); TYPED_TEST(PrimeTableTest, ReturnsTrueForPrimes) { EXPECT_TRUE(this->table_->IsPrime(2)); // ... }(L94-L132)模板世界里访问夹具成员需要显式写this->。Google Test 会为类型列表中的每种类型重复执行每个TYPED_TEST。
类型参数化测试(type-parameterized tests)——写测试时尚不知有哪些实现(典型场景:你是接口作者,希望未来所有实现都满足基础要求)。流程多三步:
template <class T> class PrimeTableTest2 : public PrimeTableTest<T> {}; TYPED_TEST_CASE_P(PrimeTableTest2); TYPED_TEST_P(PrimeTableTest2, ReturnsTrueForPrimes) { /* 同上 */ } REGISTER_TYPED_TEST_CASE_P( PrimeTableTest2, ReturnsFalseForNonPrimes, ReturnsTrueForPrimes, CanGetNextPrime); typedef Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable> PrimeTableImplementations; INSTANTIATE_TYPED_TEST_CASE_P(OnTheFlyAndPreCalculated, // 实例名 PrimeTableTest2, // 用例名 PrimeTableImplementations); // 类型列表(L160-L222)要点:REGISTER_TYPED_TEST_CASE_P必须枚举测试名,把抽象“测试模式”登记下来;INSTANTIATE_TYPED_TEST_CASE_P把模式实例化为真实测试,实例名会成为用例名的一部分,可用于测试过滤器;测试模式通常放在.h里,任何地方#include后都能实例化,甚至同一程序里实例化多次。整个 typed 段由#if GTEST_HAS_TYPED_TEST/#if GTEST_HAS_TYPED_TEST_P宏保护(L77、L140),因为部分编译器不支持这些特性。
Sample #7:值参数化测试基础
sample7_unittest.cc 展示值参数化(value-parameterized)测试的入门形态:每个测试带一个参数(这里是“被测对象的工厂函数指针”),每个参数值各跑一轮。文件头注释给出通用原则:为防止测试相互影响,应为每个测试单独创建/销毁被测对象,而不是复用——因此样例定义了工厂函数,并在夹具的SetUp()/TearDown()中分配和释放对象:
typedef PrimeTable* CreatePrimeTableFunc(); PrimeTable* CreateOnTheFlyPrimeTable() { return new OnTheFlyPrimeTable(); } 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_; };(L53-L79)在测试体、夹具构造函数、SetUp()、TearDown()中都可以用GetParam()取到参数;本例参数是工厂函数,在SetUp()里调用它创建PrimeTable。用TEST_P定义测试(L81-L106),最后用INSTANTIATE_TEST_CASE_P绑定具体参数值:
INSTANTIATE_TEST_CASE_P( OnTheFlyAndPreCalculated, PrimeTableTest, Values(&CreateOnTheFlyPrimeTable, &CreatePreCalculatedPrimeTable<1000>));(L115-L118)参数值列表也可以在别的编译单元给出,甚至多次实例化。整个文件由#if GTEST_HAS_PARAM_TEST保护;在值参数化测试不可用的平台上,#else分支里放一个空的 dummy test(L120-L128),注释解释了原因:若条件编译把所有引用gtest_main的代码都裁掉,MSVC 链接器不会链接该库,会报“入口点未定义”(fatal error LNK1561),dummy test 能保住gtest_main的链接。
Sample #8:用 Combine() 生成参数组合
sample8_unittest.cc 解决“代码依赖多个全局 flag,需要覆盖所有组合”的问题。样例构造了一个HybridPrimeTable(L50-L81),它内嵌OnTheFlyPrimeTable与PreCalculatedPrimeTable并按输入范围择用,且构造函数接受两个 flag:force_on_the_fly(bool,是否禁用预计算表)和max_precalculated(int,预计算表容量)。
夹具以二元组::testing::tuple<bool, int>作为参数类型,在SetUp()中解包并构造被测对象:
class PrimeTableTest : public TestWithParam< ::testing::tuple<bool, int> > { protected: virtual void SetUp() { bool force_on_the_fly = ::testing::get<0>(GetParam()); int max_precalculated = ::testing::get<1>(GetParam()); table_ = new HybridPrimeTable(force_on_the_fly, max_precalculated); } virtual void TearDown() { delete table_; table_ = NULL; } HybridPrimeTable* table_; };(L93-L113)注释提到,等 C++ 风格指南允许::std::tr1::tie后可改写为tie(force_on_the_fly, max_precalculated) = GetParam()。实例化时用Combine()对每个维度做笛卡尔积:
INSTANTIATE_TEST_CASE_P(MeaningfulTestParameters, PrimeTableTest, Combine(Bool(), Values(1, 10)));(L159-L161)参数选取也有讲究(见 L153-L158 注释):取1(小值)和10(使部分被测数字落在预计算表能力范围外)两个有意义的值,与Bool()的 true/false 组合共 4 种参数,恰好覆盖“预计算表启用/禁用”ד表内/表外”的全部代码路径。同样有#if GTEST_HAS_COMBINE保护与 dummy test 兜底(L41、L163-L171)。
Sample #9:Listener API 定制输出 + 反射 API 检查结果
sample9_unittest.cc 演示两件高级事情:用 Listener API 替换 Google Test 的控制台输出,用 UnitTest 反射 API 枚举用例/测试并检查结果。
Listener 部分:继承EmptyTestEventListener实现各事件回调,得到精简输出器TersePrinter(L52-L91):
OnTestProgramStart(const UnitTest&):任何测试活动开始前;OnTestStart(const TestInfo&)/OnTestEnd(const TestInfo&):单个测试开始/结束;OnTestPartResult(const TestPartResult&):某条断言失败或SUCCEED()触发后,可读取file_name()、line_number()、summary()等;OnTestProgramEnd(const UnitTest&):全部测试结束后,用unit_test.Passed()打印总结果。
配套的三个测试分别演示普通输出、SUCCEED() << "..."、以及故意失败(EXPECT_EQ(1, 2) << "...")(L93-L104)。
接线部分在main()(L108-L136):先InitGoogleTest(&argc, argv)解析参数;当用户传入--terse_output时,取出TestEventListeners&,通过
delete listeners.Release(listeners.default_result_printer());释放并移除默认控制台输出监听器(Release会把所有权转移给调用者,所以调用者必须 delete),再listeners.Append(new TersePrinter)挂上自定义监听器——加入列表后所有权归 Google Test,无需手动释放。
反射 API 部分:RUN_ALL_TESTS()之后,通过UnitTest::GetInstance()遍历所有 test case 与 test,检查每条测试的通过/失败状态并汇总打印(L139 起)。这展示了在不重新运行测试的情况下程序化地读取执行结果。
Sample #10:Listener API 实现简易泄漏检查
sample10_unittest.cc 展示了 Listener 的另一个典型用途:给测试加一个“不泄漏Water对象”的守卫。实现分三层:
- 可追踪的类:
Water重写了operator new/operator delete,维护静态计数器allocated_,并提供Water::allocated()查询(L51-L72)。 - 监听器:
LeakChecker在OnTestStart记录测试前的存活对象数,在OnTestEnd比较差值并断言:
virtual void OnTestEnd(const TestInfo& /* test_info */) { int difference = Water::allocated() - initially_allocated_; // You can generate a failure in any event handler except // OnTestPartResult. EXPECT_LE(difference, 0) << "Leaked " << difference << " unit(s) of Water!"; }(L78-L96)注意注释:除了OnTestPartResult之外的任意事件处理器里都可以产生断言失败。 3.按需启用:main()中解析自定义参数--check_for_leaks,启用时才listeners.Append(new LeakChecker)(L112-L143)。测试DoesNotLeak(new 后 delete)应通过,LeaksWater(只 new 不 delete)在开启检查时失败。
一个容易踩坑的细节是监听器的顺序语义(L131-L137):追加到列表末尾的LeakChecker,收到OnXyzStart事件时晚于它前面的监听器,收到OnXyzEnd事件时早于它们。正因为如此,LeakChecker::OnTestEnd()里产生的失败会先被默认文本输出器和 XML 报告器处理、再轮到它们的OnTestEnd(),从而被正确归因到当前测试。
小结:如何把这些样例用于实际开发
回到 V1_7_Samples.md 的定位,这 10 个文件实际上构成了 Google Test 的核心特性地图:#1/#4 覆盖TEST基本用法与断言求值语义,#2 覆盖多方法类的组织方式,#3/#5 覆盖夹具与夹具继承,#6/#7/#8 覆盖接口测试的三大参数化手段,#9/#10 覆盖 Listener API 的两个代表性应用。结合 README.md 的构建说明,最常用的路径是cmake -Dgtest_build_samples=ON ${GTEST_DIR}后编译运行;若只需验证最小样例,make/目录下的 Makefile 可直接产出sample1_unittest。需要提醒的是,本文示例代码基于仓库内置的 gtest 1.7 一代 API(如TYPED_TEST_CASE、INSTANTIATE_TEST_CASE_P),若升级到更新版本的 gtest,这些宏的命名可能有变化,请以对应版本的头文件注释为准;同时这些宏的可用性受编译器约束,样例中GTEST_HAS_TYPED_TEST、GTEST_HAS_PARAM_TEST、GTEST_HAS_COMBINE等条件编译宏正是为跨平台可编译性而设。
【免费下载链接】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),仅供参考