ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

yaml-cpp 随附的 Googletest 1.16 官方样例详解:从 TEST 宏到监听器与反射 API

yaml-cpp 随附的 Googletest 1.16 官方样例详解:从 TEST 宏到监听器与反射 API yaml-cpp 随附的 Googletest 1.16 官方样例详解从 TEST 宏到监听器与反射 API【免费下载链接】yaml-cppA YAML parser and emitter in C项目地址: https://gitcode.com/GitHub_Trending/ya/yaml-cpp本篇技术指南围绕 yaml-cpp 仓库内随附的 googletest-1.16.0 官方样例集展开逐一对 10 个samples样例进行源码级剖析覆盖基础断言、测试固件Test Fixture、类型参数化测试、值参数化测试、Combine()组合参数生成、Listener API 与反射 API 等完整特性。读完本文你不仅能看懂每一份样例的写法与设计意图还能在编写 yaml-cpp 相关 C 测试参见 test/integration 下的测试用例时直接套用这些成熟模式。样例总览一份覆盖 Googletest 全特性的学习地图官方文档 samples.md 的作用是一份学习地图它并不展开讲解语法而是指出googletest/samples目录下 10 份逐级递进的完整可编译样例每份样例都带有详尽的注释。在 yaml-cpp 仓库中这些样例位于 test/googletest-1.16.0/googletest/samples 目录与仓库根目录的 CMakeLists.txt、test/CMakeLists.txt 一起构成了完整的测试基础设施。10 份样例按学习路径排列如下对应原文档的核心索引样例核心主题对应源码Sample #1使用 googletest 测试 C 函数的基本步骤sample1_unittest.ccSample #2对拥有多个成员函数的类编写单元测试sample2_unittest.ccSample #3使用测试固件Test Fixturesample3_unittest.ccSample #4在 googletest 框架下测试带内部状态的类sample4_unittest.ccSample #5在基类测试固件中共享测试逻辑子固件复用sample5_unittest.ccSample #6类型参数化测试Typed / Type-Parameterized Testssample6_unittest.ccSample #7值参数化测试Value-Parameterized Tests基础sample7_unittest.ccSample #8在值参数化测试中使用Combine()sample8_unittest.ccSample #9Listener API 定制控制台输出 反射 API 检查测试结果sample9_unittest.ccSample #10用 Listener API 实现简易内存泄漏检查sample10_unittest.cc样例的编译与运行环境在 yaml-cpp 项目中googletest 是通过 test/CMakeLists.txt 集成的默认情况下add_subdirectory引入googletest-1.16.0测试可执行文件yaml-cpp-tests链接GTest::gtest与GTest::gmock见 test/CMakeLists.txt。也就是说仓库内所有 googletest 特性——包括下面 10 个样例演示的宏、固件、参数化与监听器——都在 yaml-cpp 的测试体系内真实可用。如果你希望单独编译运行某个样例只需把对应sampleN_unittest.cc连同其依赖的sampleN.h/sampleN.cc/prime_tables.h等加入一个链接了 googletest 与gtest_main的 CMake 目标即可无需修改仓库内容。需要自定义入口如 Sample #9、#10 自带main()时则不链接gtest_main。Sample #1三步写出第一个单元测试sample1_unittest.cc 演示了编写 googletest 用例的标准三步流程被测试对象是 sample1.h 声明的Factorial()阶乘与IsPrime()素数判断两个自由函数。Step 1包含头文件。除了被测对象的头文件sample1.h必须包含gtest/gtest.h见 sample1_unittest.cc这是框架声明所在。Step 2用TEST宏定义测试。TEST接受两个参数测试用例名Test Case / Suite 名与测试名二者都必须是合法 C 标识符且命名中不要使用下划线。测试逻辑写在一对大括号之间内部用EXPECT_*系列断言指示成败。例如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)); }这段代码来自 sample1_unittest.cc其中蕴含两个重要约定测试分组逻辑相关的测试放入同一个测试用例FactorialTest、IsPrimeTest便于组织与结果过滤测试独立性googletest 保证每个测试恰好执行一次但不保证执行顺序因此测试结果绝不能依赖执行次序断言宏差异EXPECT_EQ(expected, actual)等价于EXPECT_TRUE((expected) (actual))区别在于失败时前者会同时打印期望值与实际值调试信息更丰富而EXPECT_TRUE接受任意布尔表达式更通用。示例中以EXPECT_EQ为优先失败时输出更友好。IsPrimeTest用例sample1_unittest.cc则补充演示了EXPECT_FALSE、EXPECT_TRUE的边界输入负数、INT_MIN、0、1 与素数/合数。Step 3调用RUN_ALL_TESTS()。这一步通常通过链接src/gtest_main.cc完成——它内置一个调用RUN_ALL_TESTS()的main()会运行所有已定义测试、打印结果成功返回 0、失败返回 1。值得强调的是你不需要手动注册任何测试RUN_ALL_TESTS()会自动发现所有TEST宏定义的用例。Sample #2为多成员函数类编写更复杂的单元测试sample2_unittest.cc 演示如何测试一个拥有多个成员函数的类MyString定义见 sample2.h实现见 sample2.cc。样例的指导思想是理想情况下每个成员方法对应一个测试这并非硬性规定但有助于保持测试组织清晰如有需要可补充额外测试。样例针对MyString的四个构造/操作场景各写了一个TESTTEST(MyString, DefaultConstructor) { const MyString s; EXPECT_STREQ(nullptr, 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, 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(nullptr); EXPECT_STREQ(nullptr, s.c_string()); }源码见 sample2_unittest.cc其中的关键细节EXPECT_STREQ用于 C 字符串比较而非EXPECT_EQ。样例注释解释了原因早期 GCC 3.4 上若写EXPECT_EQ(NULL, ...)会因NULL被宏定义为整数 0 而产生警告——EXPECT_EQ需要知道实参类型以便失败时打印编译器据此选择int格式化函数而 gcc 认为NULL应作为指针使用。C 无法区分整数 0 与空指针常量是根因因此 C 风格字符串断言应使用EXPECT_STREQ。Set()的自赋值安全MyString::Set实现sample2.cc先克隆新字符串再释放旧内存因此s.Set(s.c_string())这种原地更新场景也必须有正确行为测试专门覆盖了这一点。Sample #3用测试固件消除重复的初始化代码sample3_unittest.cc 引入 googletest 的核心进阶特性——测试固件Test Fixture。测试固件用于持有同一测试用例内所有测试共享的对象与函数避免每个测试重复编写初始化/清理代码也适合存放被高频调用的子例程。固件的使用分三步从testing::Test派生一个类成员放在protected段以便子类访问需要时覆写SetUp()每个测试运行前调用用于初始化与TearDown()每个测试运行后调用用于清理用TEST_F而非TEST定义测试TEST_F的第一个参数必须与固件类名一致。class QueueTestSmpl3 : public testing::Test { protected: void SetUp() override { q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); } static int Double(int n) { return 2 * n; } void MapTester(const Queueint* q) { const Queueint* const new_q q-Map(Double); ASSERT_EQ(q-Size(), new_q-Size()); for (const QueueNodeint* n1 q-Head(), *n2 new_q-Head(); n1 ! nullptr; n1 n1-next(), n2 n2-next()) { EXPECT_EQ(2 * n1-element(), n2-element()); } delete new_q; } Queueint q0_; Queueint q1_; Queueint q2_; }; TEST_F(QueueTestSmpl3, DefaultConstructor) { EXPECT_EQ(0u, q0_.Size()); } TEST_F(QueueTestSmpl3, Dequeue) { int* n q0_.Dequeue(); EXPECT_TRUE(n nullptr); n q1_.Dequeue(); ASSERT_TRUE(n ! nullptr); EXPECT_EQ(1, *n); EXPECT_EQ(0u, q1_.Size()); delete n; }以上摘自 sample3_unittest.cc有几个必须理解的语义固件是代码共享而非数据共享每个测试都会得到固件的一份全新拷贝某个测试修改的数据不会传给下一个测试。这是刻意设计——测试必须独立、可重复一个测试不应因另一个测试的失败而失败若两个测试确实依赖先后关系它们本质上应合并为一个大测试。断言宏不能用于全局函数EXPECT_*、FAIL等宏在技术实现上会调用Test类的成员函数因此必须在固件或测试体内使用这正是把MapTester这类公共子例程放进固件的原因。也正因如此断言同样可以在SetUp()与TearDown()中使用。ASSERT_*与EXPECT_*的区别ASSERT_TRUE失败会立即终止当前测试并跳过其TearDown而EXPECT_*失败只记录失败、继续执行。例如Dequeue中先ASSERT_TRUE(n ! nullptr)再解引用*n就是为了防止空指针解引用。Sample #4无需固件、直接测试带状态的类sample4_unittest.cc 对应原文档中将 googletest 与被测代码搭配使用、兼取两者之长的说明。当前仓库版本中的该样例聚焦于直接对带内部状态的类Countersample4.h编写测试展示了一个关键事实简单场景下不需要固件TEST内自行创建对象即可并且由于每个TEST独立运行对象状态天然相互隔离。TEST(Counter, Increment) { Counter c; EXPECT_EQ(0, c.Decrement()); // 初始为 0Decrement 返回当前值并自减 EXPECT_EQ(0, c.Increment()); EXPECT_EQ(1, c.Increment()); EXPECT_EQ(2, c.Increment()); EXPECT_EQ(3, c.Decrement()); }源码见 sample4_unittest.cc。样例注释特别强调EXPECT_EQ()对其实参恰好求值一次因此实参可以携带副作用——这正是上面Increment()/Decrement()返回当前值后自增/自减能够连续断言的前提。这一特性让 googletest 可以安全地对读取并修改类接口进行逐步断言是编写有状态类测试时必须掌握的细节。Sample #5把共享逻辑提升到基类固件sample5_unittest.cc 演示固件的继承与复用。核心动机一个固件通过TEST_F只能绑定一个测试用例当多个测试用例需要相同或相近的固件时例如 GUI 库的所有测试都要检查是否泄漏字体、画刷等系统资源应把公共逻辑放进一个超类固件再让各用例的固件派生自它。样例构造了QuickTest超类固件用断言给所有派生测试加上了耗时上限class QuickTest : public testing::Test { protected: void SetUp() override { start_time_ time(nullptr); } void TearDown() override { const time_t end_time time(nullptr); EXPECT_TRUE(end_time - start_time_ 5) The test took too long.; } time_t start_time_; };摘自 sample5_unittest.cc。随后IntegerFunctionTest : public QuickTest {}与QueueTest : public QuickTest两个子固件分别绑定各自的测试用例TEST_F(IntegerFunctionTest, Factorial)、TEST_F(QueueTest, Dequeue)等。要点超类固件QuickTest本身不必有同名测试用例仅作为其他固件的基类存在是合法的子固件覆写SetUp()时须显式调用QuickTest::SetUp()见 sample5_unittest.ccTearDown()若不覆写则自动继承基类行为固件继承层次没有深度限制可以从派生固件继续派生但实践中不宜过深以免混淆。Sample #6类型参数化——一份测试模板跑遍多个实现sample6_unittest.cc 面向同一个接口的多个实现即接口测试展示了 googletest 的两种类型参数化机制。被测对象是PrimeTable接口及其两个实现OnTheFlyPrimeTable、PreCalculatedPrimeTable见 prime_tables.h。方式一Typed Tests编译期类型已知。当你写测试时已确定要覆盖的全部类型使用TYPED_TEST_SUITETYPED_TESTtypedef TypesOnTheFlyPrimeTable, PreCalculatedPrimeTable Implementations; TYPED_TEST_SUITE(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)); }源码见 sample6_unittest.cc。模板世界的两个习惯用法在测试体内通过TypeParam引用当前类型参数由于固件是类模板访问其成员必须显式写this-。googletest 会为类型列表中的每个类型重复运行每个TYPED_TEST无需手动复制用例。方式二Type-Parameterized Tests类型未知时可扩展。当你编写接口作者视角的测试、尚不清楚未来会有哪些实现时使用额外多两个步骤TYPED_TEST_SUITE_P(PrimeTableTest2); // 1. 声明模式 TYPED_TEST_P(PrimeTableTest2, CanGetNextPrime) { ... } // 2. 定义测试 REGISTER_TYPED_TEST_SUITE_P( // 3. 登记测试名 PrimeTableTest2, ReturnsFalseForNonPrimes, ReturnsTrueForPrimes, CanGetNextPrime); typedef TypesOnTheFlyPrimeTable, PreCalculatedPrimeTable PrimeTableImplementations; INSTANTIATE_TYPED_TEST_SUITE_P( // 4. 绑定真实类型 OnTheFlyAndPreCalculated, PrimeTableTest2, PrimeTableImplementations);对应源码 sample6_unittest.cc。模式通常放在.h文件中任何第三方实现者#include后即可为自己的实现实例化同一套测试同一模式可在同一程序中多次实例化实例名如OnTheFlyAndPreCalculated会进入测试用例名并可用于测试过滤。Sample #7值参数化测试基础sample7_unittest.cc 演示值参数化测试Value-Parameterized Tests每个测试携带一个运行时参数参数类型由你指定。样例延续PrimeTable接口测试场景但参数是工厂函数指针——每个测试通过参数决定构造哪种PrimeTable实现。class PrimeTableTestSmpl7 : public TestWithParamCreatePrimeTableFunc* { public: ~PrimeTableTestSmpl7() override { delete table_; } void SetUp() override { table_ (*GetParam())(); } void TearDown() override { delete table_; table_ nullptr; } protected: PrimeTable* table_; }; TEST_P(PrimeTableTestSmpl7, ReturnsFalseForNonPrimes) { EXPECT_FALSE(table_-IsPrime(-5)); ... } INSTANTIATE_TEST_SUITE_P(OnTheFlyAndPreCalculated, PrimeTableTestSmpl7, Values(CreateOnTheFlyPrimeTable, CreatePreCalculatedPrimeTable1000));摘自 sample7_unittest.cc。要点固件从TestWithParamT派生T即参数类型测试体内用GetParam()取当前参数值测试用TEST_P定义_P代表 Parameterized/Pattern用例名须与固件名一致必须用INSTANTIATE_TEST_SUITE_P(实例名, 用例名, 参数列表)实例化参数列表用Values(...)给出实例化与定义可以位于不同翻译单元也可多次实例化每个测试都会收到独立的参数值且为避免测试间相互影响被测对象应在每个测试内创建与销毁本例在SetUp/TearDown中完成。Sample #8用 Combine() 生成参数组合矩阵sample8_unittest.cc 演示如何用Combine()穷举多维度参数的所有组合。场景是一个HybridPrimeTablesample8_unittest.cc它内部同时持有OnTheFlyPrimeTable与可选的PreCalculatedPrimeTable在低内存条件下可以禁用预计算表。要覆盖全部代码路径需要同时变化是否强制 on-the-fly布尔与预计算上限整数两个维度。class PrimeTableTest : public TestWithParam ::std::tuplebool, int { protected: void SetUp() override { bool force_on_the_fly; int max_precalculated; std::tie(force_on_the_fly, max_precalculated) GetParam(); table_ new HybridPrimeTable(force_on_the_fly, max_precalculated); } ... }; TEST_P(PrimeTableTest, ReturnsFalseForNonPrimes) { ... } INSTANTIATE_TEST_SUITE_P(MeaningfulTestParameters, PrimeTableTest, Combine(Bool(), Values(1, 10)));摘自 sample8_unittest.cc。参数类型是std::tuplebool, intSetUp()用std::tie解包。Combine(Bool(), Values(1, 10))会生成全部 2×24 种组合(false,1)、(false,10)、(true,1)、(true,10)分别覆盖预计算表可用/不可用 × 上限小/大的边界。选值本身也经过斟酌max_precalculated取 1多数被测数字超出能力与 10部分数字在能力内、部分在外从而让每条代码路径都被真实命中。Combine是构造组合测试矩阵最简洁的入口尤其适合处理多个布尔开关 若干关键取值的配置类代码——yaml-cpp 解析/发射配置的测试场景即可借鉴此模式。Sample #9Listener API 定制输出 反射 API 检查结果sample9_unittest.cc 是两份系统级样例之一涉及两大 APIListener API 定制控制台输出。样例实现TersePrinter : public EmptyTestEventListenersample9_unittest.cc覆写OnTestProgramStart/End、OnTestStart/End、OnTestPartResult等回调输出极简的测试过程与失败信息含文件名、行号与摘要。随后在自定义main()中处理--terse_output命令行参数if (terse_output) { TestEventListeners listeners unit_test.listeners(); // 摘除默认控制台监听器所有权转移给调用者需 delete delete listeners.Release(listeners.default_result_printer()); // 追加自定义监听器googletest 接管所有权无需手动删除 listeners.Append(new TersePrinter); }见 sample9_unittest.cc。两个所有权语义必须牢记Release()会把监听器所有权交给调用者因此要deleteAppend()则把所有权交给 googletest。反射 API 检查测试结果。main()在RUN_ALL_TESTS()之后遍历测试结果sample9_unittest.cc通过UnitTest::GetInstance()-total_test_suite_count()与GetTestSuite(i)遍历测试套件再经test_suite.total_test_count()与GetTestInfo(j)遍历单个测试最后用test_info.result()-Failed()判断失败。样例据此统计非预期失败的用例数名字含Fails的测试是被故意写失败的并据此修正进程返回码——这是把预期失败的测试纳入回归又不影响 CI 结论的经典手法。Sample #10用 Listener API 实现简易内存泄漏检查sample10_unittest.cc 展示 Listener API 的另一典型用途在每个测试前后统计存活对象数差值大于 0 即判定泄漏。被测类Water通过重载operator new/operator delete维护静态计数器allocated_sample10_unittest.cc。监听器LeakChecker在OnTestStart记录初始计数在OnTestEnd比较差值并断言class LeakChecker : public EmptyTestEventListener { private: void OnTestStart(const TestInfo /* test_info */) override { initially_allocated_ Water::allocated(); } void OnTestEnd(const TestInfo /* test_info */) override { int difference Water::allocated() - initially_allocated_; // 除 OnTestPartResult 外任何事件回调中都可以使用断言 EXPECT_LE(difference, 0) Leaked difference unit(s) of Water!; } int initially_allocated_; };见 sample10_unittest.cc。配套的两个测试DoesNotLeak与LeaksWater后者故意泄漏一个Water对象仅在--check_for_leaks下失败验证了检查器行为。安装监听器时样例用listeners.Append(new LeakChecker)将检查器追加到列表末尾——这很关键监听器按顺序接收事件位于末尾的LeakChecker会在默认文本/XML 打印器的OnTestEnd之前收到结束事件从而把泄漏失败正确归属到对应测试名下sample10_unittest.cc。该模式说明任何资源计数 前后差值断言的检查都可以挂进监听器管道无需侵入测试代码。样例之外的进阶资源与仓库内实践除 10 份样例之外googletest-1.16.0 的官方文档目录还提供了成体系的配套资料可作为继续深入的路标primer.mdgoogletest 入门教程系统讲解断言、测试固件与TEST/TEST_F语义advanced.md进阶主题覆盖死亡测试、测试过滤、参数化测试细节等faq.md 与 samples.md 配套的 actions.md、assertions.md 等参考手册。回到 yaml-cpp 本身这些样例所演示的技法正是其测试体系test/CMakeLists.txt 中链接GTest::gtest与GTest::gmock的yaml-cpp-tests目标的基石yaml-cpp 自身的测试分布在 test/integration如 emitter_test.cpp、load_node_test.cpp、handler_test.cpp与 test/node/node_test.cpp 等文件中大量使用TEST/TEST_F、断言宏与固件组织方式test/googletest-1.16.0/docs/quickstart-cmake.md 则说明了如何在独立项目中通过 CMake 引入 googletest 并运行这类测试。读懂 10 份样例就等于拿到了阅读与扩写这些真实测试代码的钥匙。总结一条从入门到框架定制的学习路径googletest 的 10 份官方样例构成了一条精心设计的学习曲线Sample #1–#2掌握TEST宏与EXPECT_*/ASSERT_*/EXPECT_STREQ断言体系覆盖函数级与类级测试Sample #3–#5掌握固件及其继承解决初始化复用与跨用例共享逻辑Sample #6–#8掌握三类参数化测试用一份测试驱动多个类型、多组取值乃至多维度组合Sample #9–#10进入框架定制层通过 Listener API 改写输出、注入泄漏检查通过反射 API 在运行后分析测试结果。无论你是刚接触 googletest 的新手还是需要为 yaml-cpp 编写复杂测试的进阶用户都可以按此顺序逐一阅读 samples 目录下的对应源码——每份文件都自带详实注释是仓库内最完整、最贴近实战的 C 测试教科书。【免费下载链接】yaml-cppA YAML parser and emitter in C项目地址: https://gitcode.com/GitHub_Trending/ya/yaml-cpp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表