尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

miniblink49 内嵌 Google Test 1.6 官方 10 个示例全解析:从 TEST 宏到监听器 API 的 C++ 单元测试进阶之路

miniblink49 内嵌 Google Test 1.6 官方 10 个示例全解析:从 TEST 宏到监听器 API 的 C++ 单元测试进阶之路 miniblink49 内嵌 Google Test 1.6 官方 10 个示例全解析从 TEST 宏到监听器 API 的 C 单元测试进阶之路【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49Google Testgtest是 Google 开源的 C 单元测试框架miniblink49 在 v8_7_5/testing/gtest 目录下完整内嵌了 gtest 1.6 版本的源码、构建配置与官方示例。其中 V1_6_Samples.md 是官方示例的索引文档逐一罗列了 samples 目录 下 10 个带详尽注释的示例。本文以该文档为骨架结合每个示例的实际源码系统讲解 gtest 从最基本的TEST宏到测试夹具fixture、参数化测试、Combine()组合参数以及监听器listenerAPI 的完整能力栈读者学完后可以直接在 miniblink49 的 V8/内核测试体系中上手编写、组织与增强 C 单元测试。一、文档与示例总体结构V1_6_Samples.md的核心内容是一份 10 条目的示例清单每个条目对应v8_7_5/testing/gtest/samples/下的一个*_unittest.cc文件由浅入深覆盖 gtest 的主要特性。汇总如下序号示例文件核心技术点#1sample1_unittest.cc使用 gtest 测试 C 函数的基本步骤TEST宏三步走#2sample2_unittest.cc对含多个成员函数的类编写单元测试#3sample3_unittest.cc测试夹具test fixtureSetUp/TearDown与TEST_F#4sample4_unittest.cc基础示例断言参数只求值一次、允许副作用#5sample5_unittest.cc通过派生子夹具复用测试夹具fixture 继承链#6sample6_unittest.cc类型化测试typed tests与类型参数化测试type-parameterized tests#7sample7_unittest.cc值参数化测试value-parameterized tests基础#8sample8_unittest.cc值参数化测试中使用Combine()生成参数组合#9sample9_unittest.cc监听器 API 修改控制台输出 反射 API 检查测试结果#10sample10_unittest.cc监听器 API 实现一个简易内存泄漏检查器围绕这些示例samples 目录还提供了配套的被测代码与头文件sample1.hFactorial/IsPrime、sample1.cc、sample2.h 与 sample2.ccMyString类、sample3-inl.hQueue模板类、sample4.h 与 sample4.ccCounter类以及 prime_tables.hPrimeTable接口的两种实现供 #6/#7/#8 反复使用。从源码结构看这 10 个示例构成了一个循序渐进的学习曲线前 4 个解决怎么写测试#5 解决怎么复用测试#6~#8 解决怎么用一套测试覆盖多种类型/多组取值#9~#10 则深入 gtest 的事件监听体系展示如何扩展框架本身。二、Sample #1TEST 宏三步走——最基础的单元测试sample1_unittest.cc 是 gtest 的Hello World针对 sample1.h 中声明的两个函数Factorial(int n)负数的阶乘按约定返回 1与IsPrime(int n)判断素数编写测试。文件注释把流程总结为三步走// Step 1. Include necessary header files such that the stuff your // test logic needs is declared. #include limits.h #include sample1.h #include gtest/gtest.h // gtest.h 声明了整个测试框架 // Step 2. Use the TEST macro to define your tests. TEST(FactorialTest, Negative) { EXPECT_EQ(1, Factorial(-5)); EXPECT_EQ(1, Factorial(-1)); EXPECT_GT(Factorial(-10), 0); } // Step 3. Call RUN_ALL_TESTS() in main(). // 做法链接 src/gtest_main.cc它自带一个调用 RUN_ALL_TESTS() 的 main()。三个步骤的要点如下引入头文件除被测代码外必须包含gtest/gtest.h框架的所有宏与类都在其中声明。用TEST宏定义测试TEST接收两个参数——测试用例名test case name与测试名test name。用例名用于把逻辑相关的测试归组保证每个测试恰好运行一次但不保证执行顺序因此测试结果不能依赖顺序。两个名字都必须是合法的 C 标识符且官方建议不要使用下划线。调用RUN_ALL_TESTS()运行所有已定义的测试、打印结果成功返回 0失败返回 1。注册是魔法般自动完成的——你不需要手工登记任何测试框架通过宏的静态初始化机制自动收集。示例还重点对比了两类断言宏EXPECT_EQ(expected, actual)等价于EXPECT_TRUE((expected) (actual))但失败时会同时打印期望值与实际值便于调试因此优先使用EXPECT_TRUE接受任意布尔表达式更通用。示例的测试用例覆盖了边界与典型值负数、0、正数如Factorial(8) 40320、IsPrime的INT_MIN极端输入等展示了每个用例内部再按逻辑分组的组织风格。三、Sample #2对多成员函数类编写测试sample2_unittest.cc 以MyString一个简化字符串类实现见 sample2.h 与 sample2.cc为例演示如何为一个含多个成员函数的类组织测试。官方建议的惯例是类的每个方法对应一个测试不必严格一一对应但有助于保持组织性并按需补充额外用例。示例为MyString的四个行为各写了测试默认构造函数、接受 C 字符串的构造函数、拷贝构造函数、Set方法。其中Set测试特别验证了三个场景常规设置、输入指针与对象内部已有指针相同自赋值场景、以及Set(NULL)置空。TEST(MyString, Set) { MyString s; s.Set(kHelloString); EXPECT_EQ(0, strcmp(s.c_string(), kHelloString)); // Set should work when the input pointer is the same as the one // already in the MyString object. s.Set(s.c_string()); EXPECT_EQ(0, strcmp(s.c_string(), kHelloString)); // Can we set the MyString to NULL? s.Set(NULL); EXPECT_STREQ(NULL, s.c_string()); }示例中还有一个值得注意的坑断言空指针时应写EXPECT_STREQ(NULL, s.c_string())而非EXPECT_EQ(NULL, ...)。原因是EXPECT_EQ需要知道实参类型以便失败时格式化打印而NULL被#define为 0编译器会按int选择格式化函数gcc 3.4 对此会产生警告。这是 C 中整数 0 与空指针常量缺乏区分的经典副作用。四、Sample #3测试夹具Test Fixture与 TEST_Fsample3_unittest.cc 引入 gtest 的核心进阶特性——测试夹具。夹具用来安置一个测试用例内所有测试共享的对象与函数避免为每个测试重复编写初始化/清理代码也适合存放测试高频调用的子例程。被测对象是 sample3-inl.h 中的Queueint模板队列。class QueueTest : public testing::Test { protected: // 成员应为 protected便于子类访问 virtual void SetUp() { q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); } // virtual void TearDown() { } // 无清理工作时可以省略 static int Double(int n) { return 2 * n; } void MapTester(const Queueint* q) { /* 用 ASSERT_EQ/EXPECT_EQ 校验 Map 结果 */ } Queueint q0_; Queueint q1_; Queueint q2_; }; // 使用夹具的测试用 TEST_F 代替 TEST TEST_F(QueueTest, DefaultConstructor) { EXPECT_EQ(0u, q0_.Size()); }夹具的三个关键语义源码注释中的TechnicalDetails有明确说明共享的是代码而非数据每个测试都会获得一份全新的夹具实例一个测试修改的数据不会传给另一个测试。测试必须相互独立、可重复——一个测试不应因另一个测试失败而失败若两个测试依赖彼此产出的数据它们本质上应合并为一个测试。SetUp()在每个测试开始前调用TearDown()在每个测试结束后调用前者负责初始化共享变量后者负责清理均可按需省略。断言宏只能在测试上下文内使用EXPECT_TRUE、FAIL等宏会调用Test类的成员函数以便框架知道失败属于哪个测试因此不能在全局函数中使用。这也正是测试子例程应放进夹具的根本原因。示例还展示了ASSERT_*与EXPECT_*的搭配ASSERT_TRUE(n ! NULL)在失败时立即终止当前测试避免对空指针继续解引用而EXPECT_EQ失败后测试仍继续执行两者按需混用是 gtest 的常见写法。五、Sample #4断言实参只求值一次sample4_unittest.cc 是最短的示例测试 sample4.h 中的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()); }它强调了一个易被忽略的实现细节EXPECT_EQ()对实参只求值一次因此实参可以安全地携带副作用这里Increment()每次调用都会修改内部状态。如果断言宏对实参多次求值这段代码将无法正确表达预期行为。六、Sample #5用夹具继承链复用测试逻辑sample5_unittest.cc 解决多个测试用例需要相同/相近夹具的问题一个夹具在定义时绑定了唯一的测试用例名因而只能被一个用例使用。gtest 的解法是把共享逻辑放进超夹具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_; };随后派生出IntegerFunctionTest直接继承无需额外逻辑和QueueTest在QuickTest::SetUp()基础上追加自己的初始化class IntegerFunctionTest : public QuickTest { /* 空实现即可 */ }; class QueueTest : public QuickTest { protected: virtual void SetUp() { QuickTest::SetUp(); // 先初始化超夹具 q1_.Enqueue(1); // 再初始化本夹具 q2_.Enqueue(2); q2_.Enqueue(3); } Queueint q0_, q1_, q2_; };派生夹具的TearDown()默认继承超夹具行为无额外清理工作时可省略。官方注释还指出夹具继承层级没有深度限制可以从派生夹具继续派生但实践中不宜过深以免难以理解。这个超夹具 派生模式非常适合注入通用约束如资源泄漏检查、耗时上限、环境准备可以类比到 miniblink49 这类内核项目中每个模块测试都需要先初始化渲染环境的场景。七、Sample #6类型化测试与类型参数化测试sample6_unittest.cc 的主题是接口测试interface tests对同一接口的多个实现编写一套通用测试。被测接口是 prime_tables.h 中的PrimeTable其两个实现OnTheFlyPrimeTable与PreCalculatedPrimeTable由工厂函数创建。示例先定义一个夹具类模板通过基类接口PrimeTable* const table_暴露被测对象template class T class PrimeTableTest : public testing::Test { protected: PrimeTableTest() : table_(CreatePrimeTableT()) {} virtual ~PrimeTableTest() { delete table_; } PrimeTable* const table_; };7.1 类型化测试Typed Tests类型在写测试时已全部确定当编写测试时已经知道要覆盖的全部类型用 typed teststypedef TypesOnTheFlyPrimeTable, PreCalculatedPrimeTable Implementations; TYPED_TEST_CASE(PrimeTableTest, Implementations); // 声明用例并指定类型列表 TYPED_TEST(PrimeTableTest, ReturnsFalseForNonPrimes) { // 模板世界里访问夹具成员必须显式写 this- EXPECT_FALSE(this-table_-IsPrime(-5)); // ... }框架会自动为类型列表中的每个类型重复运行每个TYPED_TEST无需手工复制粘贴测试体。细节要点用例名必须与夹具名一致在模板化的测试体内类型参数用TypeParam引用、夹具类用TestFixture引用由于是模板上下文访问夹具成员必须显式写this-。整段代码以#if GTEST_HAS_TYPED_TEST保护。7.2 类型参数化测试Type-Parameterized Tests类型可延迟到未来当不知道全部类型例如你是接口作者期待他人未来实现时用 type-parameterized tests。它比 typed tests 多几步但换来可跨上下文复用的测试模式template class T class PrimeTableTest2 : public PrimeTableTestT {}; TYPED_TEST_CASE_P(PrimeTableTest2); // _P 即 parameterized/pattern TYPED_TEST_P(PrimeTableTest2, ReturnsFalseForNonPrimes) { EXPECT_FALSE(this-table_-IsPrime(-5)); // ... } // 关键一步必须显式枚举本模式中定义的所有测试 REGISTER_TYPED_TEST_CASE_P( PrimeTableTest2, ReturnsFalseForNonPrimes, ReturnsTrueForPrimes, CanGetNextPrime); // 用类型列表把抽象模式实例化为真实测试 typedef TypesOnTheFlyPrimeTable, PreCalculatedPrimeTable PrimeTableImplementations; INSTANTIATE_TYPED_TEST_CASE_P(OnTheFlyAndPreCalculated, // 实例名 PrimeTableTest2, // 用例名 PrimeTableImplementations); // 类型列表与 typed tests 最大的差异在于延迟绑定模式通常写在.h文件中任何人#include后即可用INSTANTIATE_TYPED_TEST_CASE_P实例化甚至可在同一程序中实例化多次每次实例化需给一个唯一名称该名称会成为用例名的一部分可用于测试过滤器。整体以#if GTEST_HAS_TYPED_TEST_P保护。八、Sample #7值参数化测试基础sample7_unittest.cc 展示值参数化测试每个测试都携带一个参数本例中是PrimeTable实现工厂函数指针。核心 API 是TestWithParamT、TEST_P、GetParam()与INSTANTIATE_TEST_CASE_Ptypedef PrimeTable* CreatePrimeTableFunc(); PrimeTable* CreateOnTheFlyPrimeTable() { return new OnTheFlyPrimeTable(); } template size_t max_precalculated PrimeTable* CreatePreCalculatedPrimeTable() { return new PreCalculatedPrimeTable(max_precalculated); } class PrimeTableTest : public TestWithParamCreatePrimeTableFunc* { public: virtual ~PrimeTableTest() { delete table_; } virtual void SetUp() { table_ (*GetParam())(); } // 在 SetUp 中通过参数创建被测对象 virtual void TearDown() { delete table_; table_ NULL; } protected: PrimeTable* table_; }; TEST_P(PrimeTableTest, ReturnsFalseForNonPrimes) { EXPECT_FALSE(table_-IsPrime(-5)); // ... } // 实例化把测试绑定到一组参数值 INSTANTIATE_TEST_CASE_P( OnTheFlyAndPreCalculated, PrimeTableTest, Values(CreateOnTheFlyPrimeTable, CreatePreCalculatedPrimeTable1000));关键规则与细节测试体内、夹具构造函数、SetUp()、TearDown()中均可通过GetParam()读取当前测试参数通用原则是每个测试独立创建并销毁被测对象避免前一个测试影响后续测试因此这里把创建放进SetUp()、销毁放进TearDown()实例化可以在不同的翻译单元中进行也可以多次实例化整段以#if GTEST_HAS_PARAM_TEST保护且在平台不支持时提供一个空DummyTest——这样做的原因很微妙若把所有引用 gtest_main 库的代码都条件编译掉MSVC 链接器会因找不到该库中的入口点而报错LNK1561空测试可保持 gtest_main 被链接进来。九、Sample #8用 Combine() 生成参数组合sample8_unittest.cc 演示在值参数化测试中使用Combine()当被测代码依赖多个全局标志、需要穷举其所有组合时Combine()可以自动生成笛卡尔积。场景是测试HybridPrimeTable——一个同时持有OnTheFlyPrimeTable与PreCalculatedPrimeTable的混合实现低内存下可禁用预计算表。要覆盖全部代码路径需要同时变化是否强制 on-the-flybool与预计算上限int两个参数using ::testing::TestWithParam; using ::testing::Bool; using ::testing::Values; using ::testing::Combine; class PrimeTableTest : public TestWithParam ::testing::tuplebool, int { protected: virtual void SetUp() { bool force_on_the_fly ::testing::get0(GetParam()); int max_precalculated ::testing::get1(GetParam()); table_ new HybridPrimeTable(force_on_the_fly, max_precalculated); } virtual void TearDown() { delete table_; table_ NULL; } HybridPrimeTable* table_; }; TEST_P(PrimeTableTest, ReturnsFalseForNonPrimes) { /* 三个标准接口测试 */ } TEST_P(PrimeTableTest, ReturnsTrueForPrimes) { /* ... */ } TEST_P(PrimeTableTest, CanGetNextPrime) { /* ... */ }从源码可见夹具的参数类型是::testing::tuplebool, intSetUp()用::testing::get0/get1拆出两个参数源码注释还提到一旦 Google C 风格指南允许使用::std::tr1::tie可改写为tie(force_on_the_fly, max_precalculated) GetParam()。测试的实例化则通过Combine(Bool(), Values(...))把布尔值与一组上限值两两组合成完整参数对代码同样以GTEST_HAS_COMBINE保护。Bool()是 gtest 内置的生成器true/false 两值Values(...)用于列举离散值Combine()将它们组合成参数序列——这是测试多个开关排列组合场景的标准做法。十、Sample #9监听器 API 与反射 APIsample9_unittest.cc 按官方文档的说明展示了 gtest 事件监听体系的两个高级用途监听器 APIlistener API修改 Google Test 的控制台输出gtest 把测试生命周期中的各类事件用例开始/结束、测试开始/结束、断言失败等以事件回调的形式派发给注册的监听器框架默认的控制台打印器printer本身就是一个监听器。开发者可以派生自己的监听器替换或追加到事件链上从而自定义输出格式、增删输出内容。反射 APIreflection API检查测试结果通过UnitTest等入口对象可以在运行时枚举测试用例、测试及其结果信息实现对测试执行的程序化检查与汇总。Sample #10 是这一 API 最直接的实战应用建议对照阅读。十一、Sample #10用监听器 API 实现内存泄漏检查器sample10_unittest.cc 是监听器 API 的完整实战实现一个原始但可运行的内存泄漏检查器。其思路是让被测类Water重载operator new/operator delete统计存活实例数再通过监听器在每个测试开始/结束时对比该计数。被测类的分配统计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;自定义监听器在测试开始前记录存活数结束后对比并断言差值不为正即不允许泄漏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_; // 除 OnTestPartResult 外任何事件处理器里都可以用断言产生失败 EXPECT_LE(difference, 0) Leaked difference unit(s) of Water!; } int initially_allocated_; };在main()中按命令行开关安装监听器框架会负责在程序结束时删除它无需手工管理int main(int argc, char **argv) { InitGoogleTest(argc, argv); bool check_for_leaks false; if (argc 1 strcmp(argv[1], --check_for_leaks) 0) check_for_leaks true; if (check_for_leaks) { TestEventListeners listeners UnitTest::GetInstance()-listeners(); listeners.Append(new LeakChecker); // 追加到默认输出打印器与 XML 报告生成器之后 } return RUN_ALL_TESTS(); }两个值得吸收的工程细节追加顺序很重要Append把监听器加在默认文本打印器与 XML 报告生成器之后。由于事件回调的方向是Start事件从前向后、End事件从后向前分发这样能保证泄漏检查器在OnTestEnd()中产生的失败会先于打印器自己的OnTestEnd()被处理从而归属到正确的测试名下。配套测试中ListenersTest::LeaksWater在带--check_for_leaks运行时预期失败恰好验证了检查器的有效性——这是测试测试框架自身的绝佳范例。十二、在 miniblink49 中构建与运行这些示例miniblink49 内嵌的 gtest 位于 v8_7_5/testing/gtest它自带完整的构建体系示例代码即包含在源码树中无需单独下载GN 构建BUILD.gn 定义了 gtest 的 GN 目标可直接被 V8 及上层组件的构建引用CMake 构建CMakeLists.txt 提供了 CMake 入口适合独立构建 gtest 与 samples其他构建方式目录下还保留了 configure.ac、Makefile.am、msvcVisual Studio 工程与 xcodeXcode 工程等历史构建配置覆盖主流平台。需要说明的适用前提本仓库为 miniblink49一款基于 Chromium/Blink 的轻量浏览器内核的源码树gtest 以第三方依赖形式内嵌在 V8 源码目录v8_7_5下用于支撑 V8 及内核相关组件的单元测试。若要在本地跑通示例需先按上述构建体系编译 gtest 与 samples或在目标平台使用对应的 msvc/xcode 工程生成的可执行文件直接运行即可看到各用例的通过/失败汇总对于 sample #10需附加--check_for_leaks命令行参数以开启自定义泄漏检查。十三、小结10 个示例背后的 gtest 设计主线纵观 V1_6_Samples.md 与 samples 目录10 个示例串起了 gtest 1.6 的三条设计主线易用性#1~#4TEST宏三步走、自动注册、无需手工main链接gtest_main即可、断言失败自动打印期望值与实际值、ASSERT_*与EXPECT_*的失败语义分工复用与参数化#5~#8夹具通过继承链共享#5类型维度用 typed/type-parameterized tests 复用#6取值维度用 value-parameterized tests 与Combine()穷举#7/#8共同解决一套测试逻辑覆盖 N 个变体的规模化问题可扩展性#9~#10事件监听器让开发者能插入测试生命周期、定制输出乃至实现泄漏检查等横切能力反射 API 则允许程序化地枚举与检查测试结果。对于需要在 miniblink49 这类大型 C 代码库中编写单元测试的开发者这套示例既是 API 参考手册也是测试代码组织范式的活教材按 #1~#4 入门按 #5 组织复用按 #6~#8 压缩重复测试体按 #9~#10 扩展框架能力即可覆盖绝大多数内核级模块的测试需求。【免费下载链接】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),仅供参考
返回列表