ARTICLE DETAIL

资讯详情

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

GoogleTest FAQ 实战指南:从命名规则到疑难排查的完整解读(附 yaml-cpp 测试实践佐证)

GoogleTest FAQ 实战指南:从命名规则到疑难排查的完整解读(附 yaml-cpp 测试实践佐证) GoogleTest FAQ 实战指南从命名规则到疑难排查的完整解读附 yaml-cpp 测试实践佐证【免费下载链接】yaml-cppA YAML parser and emitter in C项目地址: https://gitcode.com/GitHub_Trending/ya/yaml-cpp本文以 GoogleTest 1.16.0 官方 FAQ仓库路径 test/googletest-1.16.0/docs/faq.md为骨架逐条拆解其核心问答测试命名规则、断言宏与 nullptr 的选择、typed tests 与 value-parameterized tests 的取舍、death tests 的进程模型、测试夹具的设计与继承以及一系列编译/链接/运行期疑难杂症。文中将结合 yaml-cpp 仓库真实测试代码如 test/integration/emitter_test.cpp、test/integration/handler_spec_test.cpp、test/mock_event_handler.h加以印证。读完本文你不仅能规避 GoogleTest 最常见的陷阱还能理解其底层实现逻辑写出更健壮、更易维护的 C 测试。一、命名规则为什么测试套件名与测试名不能包含下划线FAQ 开篇就回答了 GoogleTest 社区最经典的问题之一TEST()与TEST_F()的 TestSuiteName 和 TestName 为什么不能带下划线_1.1 C 标准保留标识符规则C 标准为编译器和标准库保留了以下两类标识符用户代码严禁使用以_开头且后跟大写字母的任意标识符名字中任意位置出现两个连续下划线__的标识符。1.2 宏展开后的类名会踩中保留规则TEST(TestSuiteName, TestName)会生成一个名为TestSuiteName_TestName_Test的类。FAQ 逐例推演了四种违规情形命名情形生成类名结果TestSuiteName以_Foo开头_Foo_TestName_Test以下划线开头且后跟大写字母保留标识符非法TestSuiteName以Foo_结尾Foo__TestName_Test含连续双下划线非法TestName以_Bar开头TestSuiteName__Bar_Test含连续双下划线非法TestName以Bar_结尾TestSuiteName_Bar__Test含连续双下划线非法严格说TestSuiteName只要_后面不是大写字母仍可开头但为了简单好记官方直接要求不能以_开头。1.3 中间下划线也不行类名冲突即便把下划线放在名字中间也可能引发严重问题。FAQ 给出两个例子TEST(Time, Flies_Like_An_Arrow) { ... } TEST(Time_Flies, Like_An_Arrow) { ... }两个TEST宏最终生成的是同一个类名Time_Flies_Like_An_Arrow_Test导致重定义冲突。1.4 结论与实践建议该规则比实际必要更严格但简单易记也为 GoogleTest 未来实现留了余量。违反规则当下也许没有报错但换编译器版本或 GoogleTest 版本后测试随时可能崩坏。因此TestSuiteName 与 TestName 一律不使用_推荐用驼峰或单词拼接命名。在 yaml-cpp 仓库中可以找到大量符合该规范的命名实例例如 test/parser_test.cpp 中的TEST(ParserTest, CVE_2017_5950)、test/regex_test.cpp 中的TEST(RegExTest, OperatorAndShortCircuits)以及 test/integration/emitter_test.cpp 中的TEST_F(EmitterTest, SimpleScalar)等——套件名全部是驼峰单词测试名用驼峰或带版本号的大写片段绝不混用_拼接。二、断言与 NULL为什么支持EXPECT_EQ(NULL, ptr)却不支持EXPECT_NE(NULL, ptr)2.1 首选直接用nullptrFAQ 明确指出对指针比较应优先使用 C11 的nullptrEXPECT_EQ(ptr, nullptr); EXPECT_NE(ptr, nullptr); ASSERT_EQ(ptr, nullptr); ASSERT_NE(ptr, nullptr);nullptr是类型安全的不存在NULL的宏/整数歧义问题NULL通常是0或0L在重载与模板推导场景会引入类型陷阱。2.2 为什么只给EXPECT_EQ/ASSERT_EQ提供 NULL 支持在 C 模板元编程层面让NULL作为EXPECT_XX()/ASSERT_XX()的实参需要相当绕的模板技巧维护成本高、易出错因此 GoogleTest 只在需求最强烈的场景实现了历史上EXPECT_EQ()的第一个参数是expected期望值、第二个是actual实际值用户确实高频写出EXPECT_EQ(NULL, some_expression)这种形式社区也多次提出请求于是实现了。而EXPECT_NE(NULL, ptr)的需求弱得多断言失败时你已知ptr必然是NULL打印ptr不增加任何信息EXPECT_TRUE(ptr ! NULL)的效果完全一样。若支持EXPECT_NE(NULL, ptr)为对称还得支持EXPECT_NE(ptr, NULL)意味着模板技巧要实现两次收益远不抵成本。2.3 大势所趋用 gMock matcher 统一断言FAQ 还点明了未来方向随着 gMock matcher 库的壮大官方鼓励用统一的EXPECT_THAT(value, matcher)语法。matcher 的优势在于可以自由组合而EXPECT_NE这类宏无法组合。yaml-cpp 仓库正是这样实践的test/integration/clone_node_test.cpp 中出现了EXPECT_THAT(key_marks, ...)test/node/node_test.cpp 中则有EXPECT_THAT(emitter.c_str(), AnyOf(Eq(output1), Eq(output2)))——把AnyOf、Eq等 matcher 组合起来表达输出可以是两种合法形式之一这正是 FAQ 所说宏难以实现的表达能力。三、同接口多实现测试typed tests 还是 value-parameterized tests当需要验证同一接口的多个实现是否满足共同需求时FAQ 给出了两种方案的取舍指南考量维度Typed testsValue-parameterized tests实例创建方式各实现可统一方式创建如都有公开默认构造或工厂函数同形CreateInstanceTypeParam()时更易写各实现创建方式不同如new Foovsnew Bar(5)时更易写——可写工厂函数包装器把函数指针作为测试参数传入失败输出默认输出包含类型名可快速定位是哪个实现出错默认只显示失败的迭代编号需自定义返回迭代名的函数作为第三个参数传给INSTANTIATE_TEST_SUITE_P才能获得有意义输出类型安全陷阱必须确保被测的是接口类型即implicit_castMyInterface*(my_concrete_impl)可编译而非具体类型在此处犯错的可能性较小FAQ 的建议很实在两种都试试实践是理解二者微妙差异的最好方式。从本仓库的 GoogleTest 源码看typed tests 的完整宏体系定义在 test/googletest-1.16.0/googletest/include/gtest/gtest-typed-test.hTYPED_TEST_SUITE(FooTest, MyTypes)声明测试套件与类型列表可加第三个参数自定义类型名TYPED_TEST(FooTest, DoesBlah)定义用例类型参数通过TypeParam访问类型参数化测试则用TYPED_TEST_SUITE_PTYPED_TEST_PREGISTER_TYPED_TEST_SUITE_PINSTANTIATE_TYPED_TEST_SUITE_P组合。而 value-parameterized 的实例化宏INSTANTIATE_TEST_SUITE_P在 test/googletest-1.16.0/googletest/test/googletest-param-test-test.cc 中有大量直接用例如INSTANTIATE_TEST_SUITE_P(Sequence1, MultipleInstantiationTest, Values(1, 2))参数生成器可用Values、Range、ValuesIn等第三个可选参数即自定义测试名生成器。四、Death Tests 深度剖析进程模型与线程纠缠4.1 子进程模型为什么修改状态会丢失EXPECT_DEATH等 death test 在子进程中执行让预期的崩溃不会杀掉测试程序父进程。因此死亡测试语句产生的内存副作用只存在于子进程父进程完全看不到如果死亡测试语句调用了 mock 方法父进程会认为这些调用从未发生——所以把EXPECT_CALL写进EXPECT_DEATH宏内部是个常用技巧。可以通俗地理解为death test 运行在一个平行宇宙里。4.2 线程问题挂起或段错误的修复思路death tests 的工作机制很脆弱FAQ 给出系统性的排查路径首选消除父进程中的多余线程——death test 不喜欢父进程存在多线程。优先用 mock 或 fake 对象替代真实对象。若某个库在main()之前就创建了线程无法避免则要么把尽可能多的活动移入EXPECT_DEATH()极端情况下全部移入要么反过来尽量少放减少冲突面。可尝试把 death test 风格设为threadsafe更安全但更慢。注意 threadsafe 模式会在子进程中从头重跑整个测试程序因此必须保证程序可自我并行运行、且结果确定无竞态、无死锁。FAQ 最后总结这本质上是并发编程问题没有银弹。4.3ASSERT_DEATH的 statement 可以是任意合法语句ASSERT_DEATH(statement, matcher)中statement只要是当前上下文中合法的 C 语句即可可以是简单函数调用、复杂表达式、甚至复合语句且可引用全局/局部变量。FAQ 给出四类示例// 简单函数调用 TEST(MyDeathTest, FunctionCall) { ASSERT_DEATH(Xyz(5), Xyz failed); } // 引用变量和函数的复杂表达式 TEST(MyDeathTest, ComplexExpression) { const bool c Condition(); ASSERT_DEATH((c ? Func1(0) : object2.Method(test)), (Func1|Method) failed); } // 可以放在循环里 TEST(MyDeathTest, InsideLoop) { for (int i 0; i 5; i) { EXPECT_DEATH_M(Foo(i), Foo has \\d errors, ::testing::Message() where i is i); } } // 复合语句 TEST(MyDeathTest, CompoundStatement) { ASSERT_DEATH({ for (int i 0; i 5; i) { Bar(i); } }, Bar has \\d errors); }注意 matcher 是正则表达式数字要写成\\d转义形式。4.4 为什么含 death test 的整个测试套件要命名*DeathTestGoogleTest不会交错执行不同测试套件的用例它先跑完一个套件的全部测试再跑下一个。这是因为套件需要在其首个测试前 setup、全部结束后 teardown拆散会带来多次 setup/teardown 且语义不干净。若按测试名而非套件名决定 death test 的执行顺序会与不交错产生矛盾FAQ 用FooTest.AbcDeathTest/BarTest.Xyz的例子证明了这一点。因此约定整个套件命名为*DeathTestdeath test 用例会优先于其他套件执行。如果不想整个套件都叫*DeathTest可以拆分并复用 fixtureclass FooTest : public ::testing::Test { ... }; TEST_F(FooTest, Abc) { ... } TEST_F(FooTest, Def) { ... } using FooDeathTest FooTest; // 直接复用 fixture TEST_F(FooDeathTest, Uvw) { ... EXPECT_DEATH(...) ... } TEST_F(FooDeathTest, Xyz) { ... ASSERT_DEATH(...) ... }4.5 死亡测试子进程中的 LOG 输出death test 子进程产生的 LOG 消息只在测试失败时打印因为常显会干扰父进程日志中对真实问题的检索。确实需要查看时一个临时 hack 是把期望匹配的正则改成必然失败的内容以触发失败输出。4.6 Linux pthread 的历史坑Linux 传统 pthread 库下第一次创建线程时会额外生成一个 manager 线程于是得到 3 个而非 2 个线程即使后续 join 回主线程manager 线程永不消亡父进程仍保有 2 个线程无法安全运行 death test。NPTL 新线程库没有此问题但你不能假设测试机一定用 NPTL。五、测试夹具Test Fixture继承、复用与生命周期5.1 fixture 能否继承——可以且无深度限制每个测试夹具对应一个同名测试套件一个夹具通常只被一个套件使用。若多个套件需要共享逻辑FAQ 给出的模式是把共享逻辑放进基类夹具再为每个套件派生专用夹具// 基类测试夹具 class BaseTest : public ::testing::Test { protected: ... }; // 从 BaseTest 派生 FooTest class FooTest : public BaseTest { protected: void SetUp() override { BaseTest::SetUp(); // 先设置基类夹具 ... additional set-up work ... } void TearDown() override { ... clean-up work for FooTest ... BaseTest::TearDown(); // 记得在清理完 FooTest 后再析构基类 } ... functions and variables for FooTest ... }; TEST_F(FooTest, Bar) { ... } TEST_F(FooTest, Baz) { ... }可以从派生夹具继续派生层次深度不限。yaml-cpp 的 test/integration/emitter_test.cpp 展示了真实项目中的夹具用法EmitterTest继承::testing::Test在protected区域声明了辅助方法ExpectEmit()内部用EXPECT_EQ、EXPECT_TRUE校验 emitter 输出并回灌给Parser验证可解析性和数据成员Emitter out随后数十个TEST_F(EmitterTest, ...)用例共享这份逻辑。5.2 不想为每个套件定义新夹具用 typedef共享同一夹具逻辑时不必逐个定义新类typedef BaseTest FooTest; TEST_F(FooTest, Abc) { ... } TEST_F(FooTest, Def) { ... } typedef BaseTest BarTest; TEST_F(BarTest, Abc) { ... } TEST_F(BarTest, Def) { ... }5.3 构造函数/析构函数 vsSetUp()/TearDown()FAQ 首先强调GoogleTest不会跨测试复用夹具对象。每个TEST_F都会创建一个全新的夹具对象 → 立即调用SetUp()→ 运行测试体 → 调用TearDown()→ 销毁对象。优先用构造函数/析构函数的理由成员变量可在构造函数初始化时声明为const防止误改测试更直观正确继承时子类构造函数保证先调用基类构造函数、子类析构函数保证后调用基类析构函数而SetUp()/TearDown()容易忘记调用基类版本或顺序出错。仍需用SetUp()/TearDown()的场景C 不允许在构造/析构中做虚函数调用——构造函数里调虚函数走的是当前正在构造类的定义动态派发失效因为派生类成员尚未初始化。需要调用被子类覆写的方法时只能用SetUp()/TearDown()构造/析构体内无法使用ASSERT_xx宏详见下文若 setup 失败应致命中止测试只能用SetUp()或用abort终止整个可执行文件若 tear-down 操作可能抛异常必须用TearDown()——析构函数中抛异常是未定义行为通常直接杀死程序且许多标准库在开启异常时会抛异常若要写兼容开/关异常两种编译的移植性测试应优先TearDown()GoogleTest 团队正考虑在支持异常的平台让断言宏改为抛异常届时从子过程向调用者传播失败的需求消失因此析构函数里不要放断言。5.4 夹具必须有默认构造函数TEST_F(FooTest, Bar)报no matching function for call to FooTest::FooTest()原因是 GoogleTest 必须能创建夹具对象。编译器通常自动生成默认构造但两种情况必须自己写显式声明了非默认构造函数DISALLOW_EVIL_CONSTRUCTORS()就会这么做时需补一个默认构造哪怕是空的夹具含const 非静态数据成员时必须在构造函数的初始化列表中初始化该 const 成员旧版 gcc 不强制属于已修复的 bug。5.5 为什么夹具优于全局变量测试常需修改共享状态全局变量会让副作用跨测试逃逸、互相污染、难以调试夹具让每个测试拿到一套同名但全新的变量测试彼此独立全局变量污染全局命名空间夹具可通过继承复用全局变量做不到。六、编译与链接疑难杂症速查6.1EXPECT_EQ(htonl(blah), blah_blah)在 opt 模式产生诡异编译错误FAQ 断言bug 在htonl()本身。按 man 手册htonl()是函数因此可作为函数指针使用但在优化模式下它被定义为宏且该宏实现用了 gcc 扩展、非标准 C还限制了Foosizeof(htonl(x))()这种模板用法。而EXPECT_EQ(a, b)的实现会在模板实参内使用sizeof(...)于是 opt 模式下a含htonl()调用就无法编译。此问题很难让EXPECT_EQ绕过因为解法必须跨平台适配不同编译器。6.2 static const 成员未定义引用链接错误类内声明静态数据成员必须在类外定义否则是非法 C// foo.h class Foo { static const int kBar 100; }; // foo.cc const int Foo::kBar; // 无需初始化器不这么做的话EXPECT_EQ等比较断言会触发 undefined reference 链接错误。以前能跑不代表合法只是运气好。若声明为constexpr则隐式成为 inline 定义无需在 .cc 中再定义class Foo { static constexpr int kBar 100; // 直接构成定义 };6.3 void value not ignored as it ought to be多半是在非 void 返回值的函数里用了ASSERT_*()。由于构建系统默认关闭异常ASSERT_*()只能用于返回void的函数详见 advanced.md 的断言放置说明。6.4 构造函数或析构函数cannot return a value为支持ASSERT_EQ(1, Foo()) blah foo这种流式消息语法GoogleTest 不得不在ASSERT*和FAIL*上放弃在构造/析构中的使用EXPECT*和ADD_FAILURE*不受影响。解决办法把构造/析构内容搬进私有 void 成员函数或改用EXPECT_*()。6.5ASSERT_PRED*报 no matching function to call详见 assertions.md 的EXPECT_PRED*一节。6.6 用户自定义类型用于断言时报no match for operator断言失败时需要打印参数值因此用户类型FooType必须提供std::ostream operator(std::ostream, const FooType)若类型声明在命名空间内操作符也须定义在同一命名空间。6.7 ignoring return valueRUN_ALL_TESTS()的返回值必须使用有人写成RUN_ALL_TESTS(); // 错误且危险正确写法是把返回值作为main()的返回值return RUN_ALL_TESTS();测试服务依赖该返回值判断成败忽略它会导致断言失败也被判定为测试通过。GoogleTest 已在 gcc 下用属性强制此返回值不可忽略。仓库中的 test/main.cpp 正是规范写法::testing::InitGoogleTest(argc, argv);后return RUN_ALL_TESTS();。从源码看test/googletest-1.16.0/googletest/include/gtest/gtest.h 中RUN_ALL_TESTS()声明带GTEST_MUST_USE_RESULT_标记内部实现为::testing::UnitTest::GetInstance()-Run()返回int结果码——这就是不能忽略返回值的机制根源。6.8 no matching function 的另一来源EXPECT_EQ(htonl(...))已覆盖还有SetUp()拼写SetUp()没被调用检查大小写——C 区分大小写Setup()小写 p不会触发回调。同理SetUpTestSuite()也别拼成SetupTestSuite()。七、其他高频问题与最佳实践7.1 测试输出被 LOG 淹没GoogleTest 输出刻意保持简洁。LOG 消息走 stderrGoogleTest 输出走 stdout用重定向即可分离$ ./my_test gtest_output.txt7.2 如何临时禁用某个测试给测试名加DISABLED_前缀即可排除执行。这比注释代码或#if 0更好——被禁用的测试仍然参与编译代码不会腐化。需要强制运行时加命令行标志--gtest_also_run_disabled_tests。yaml-cpp 仓库正好有真实范例test/integration/handler_spec_test.cpp 中的TEST_F(HandlerSpecTest, DISABLED_Ex6_25_InvalidVerbatimTags)以及 test/integration/node_spec_test.cpp 中的TEST(NodeSpecTest, DISABLED_Ex6_25_InvalidVerbatimTags)等。这些是针对 YAML 规范中无效标签等边界用例的用例被DISABLED_前缀暂时挂起但仍保持编译方便后续修复后随时恢复。7.3 不同命名空间下的同名TEST是否合法合法但有一个硬规则同一测试套件的所有测试方法必须使用同一夹具类。合法示例都用默认夹具::testing::Testnamespace foo { TEST(CoolTest, DoSomething) { SUCCEED(); } } // namespace foo namespace bar { TEST(CoolTest, DoSomething) { SUCCEED(); } } // namespace bar非法示例同名套件CoolTest用了foo::CoolTest与bar::CoolTest两个不同夹具类会在运行时被 GoogleTest 报错namespace foo { class CoolTest : public ::testing::Test {}; // 夹具 foo::CoolTest TEST_F(CoolTest, DoSomething) { SUCCEED(); } } // namespace foo namespace bar { class CoolTest : public ::testing::Test {}; // 夹具 bar::CoolTest TEST_F(CoolTest, DoSomething) { SUCCEED(); } } // namespace bar7.4 如何检测代码是否运行在测试中FAQ 强烈反对这种嗅探理由有三测试专用逻辑渗入生产代码、无法保证测试分支不被误跑、且容易制造 Heisenbug。GoogleTest不提供检测机制。正确做法是依赖注入生产代码根本不链接测试专用逻辑用 BUILD 目标的testonly属性Bazel从构建层面保证安全。实在万不得已且测试程序名以_test结尾可在main()里嗅探argv[0]——FAQ 自己称之为可怕的 hack。7.5 Windows 上的内存泄漏误报Visual C 内存泄漏检测器会报告静态初始化的 GoogleTest 单例泄漏因为该单例需要堆分配。可用_CrtMemCheckpoint与_CrtMemDumpAllObjectsSince组合跳过对静态初始化堆对象的报告详见 MSDN 的 heap check/debug 例程。八、FAQ 之外的延伸death test 与断言细节的完整参考FAQ 反复指向两处权威文档值得一并阅读advanced.md断言放置规则ASSERT_*与 void 函数的关系、DISABLED_前缀的完整说明#temporarily-disabling-tests小节reference/assertions.md全部断言宏的权威参考其中#death小节详解 death test 断言#EXPECT_PRED小节详解谓词断言。若想查看夹具继承的完整官方示例仓库自带 test/googletest-1.16.0/googletest/samples/sample5_unittest.cc。yaml-cpp 的 test/integration/emitter_test.cpp 与 test/integration/handler_spec_test.cpp 则是真实大型项目中夹具复用、DISABLED_挂起与 gMock matchertest/mock_event_handler.h 中MOCK_METHOD声明的完整事件处理器 mock组合使用的活教材。结语GoogleTest FAQ 的价值在于它不回避实现细节命名规则背后是 C 保留标识符与宏生成类名机制death test 的怪癖源于子进程模型各种编译错误则是模板元编程与宏展开的自然结果。理解这些底层机制后你不仅能对为什么这样写知其所以然还能在遇到诡异错误时迅速定位根因——这正是把 GoogleTest 从能用提升到用好的分水岭。结合本仓库的测试代码test/main.cpp、test/integration/、test/node/你可以在真实项目中看到这些规则如何落地。【免费下载链接】yaml-cppA YAML parser and emitter in C项目地址: https://gitcode.com/GitHub_Trending/ya/yaml-cpp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表