ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Googletest 实战 FAQ 深度解析:从命名规则到死亡测试,结合 TEN-framework 的 C++ 测试实践

Googletest 实战 FAQ 深度解析:从命名规则到死亡测试,结合 TEN-framework 的 C++ 测试实践 人工智能AI Agent多模态语音AI 应用【免费下载链接】ten-frameworkOpen-source framework for conversational voice AI agents项目地址https://gitcode.com/TEN-framework/ten-framework点击查看免费下载导读本文以 TEN-framework 仓库内置的 googletest 官方 FAQ.md 为骨架系统梳理 C 测试框架 googletest 在实际使用中最容易踩坑的二十余个高频问题——从TEST/TEST_F命名规则、断言宏的 NULL 兼容性、typed tests 与 value-parameterized tests 的选择到死亡测试death test的性能与线程问题、fixture 的继承与生命周期、各类编译期错误的成因与修复。全文以 TEN-framework 仓库中真实存在的测试代码如 tests/ten_runtime/smoke 下的 smoke 测试、gtest_main.cc 自定义入口等为佐证帮助你既理解 googletest 的底层设计意图又掌握可立即落地的调试与规避技巧。一、为什么TEST的用例名与测试名不能包含下划线1.1 C 保留标识符规则googletest 官方 FAQ 给出的第一条理由与 C 语言标准直接相关编译器与标准库保留了以下两类标识符以_开头、后接大写字母的标识符如_Foo名称中任意位置出现连续两个下划线__的标识符。用户代码被禁止使用这两类标识符。问题在于TEST(TestCaseName, TestName)宏会展开并生成一个名为TestCaseName_TestName_Test的类若TestCaseName以_开头且后接大写字母如_Foo生成的_Foo_TestName_Test属于保留标识符非法若TestCaseName以_结尾如Foo_生成的Foo__TestName_Test含连续双下划线非法若TestName以_开头或结尾同样会在名称中制造出__非法。因此TestCaseName与TestName都不能以_开头或结尾严格说TestCaseName以_开头只要后接小写字母即可但官方为简单起见统一禁止。1.2 中间出现下划线也会引发类名冲突即便下划线出现在名称中间也会带来命名冲突问题。考虑官方文档给出的例子TEST(Time, Flies_Like_An_Arrow) { ... } TEST(Time_Flies, Like_An_Arrow) { ... }两个测试宏分别展开后生成的类名都是Time_Flies_Like_An_Arrow_Test发生冲突。所以官方干脆一刀切在TestCaseName和TestName中完全避免下划线。这条规则比必要限制更严格但简单、易记也为 googletest 未来实现演进留出余地。提示违反该规则短期内可能看不出问题但换用新编译器或 googletest 新版本后测试可能莫名编译失败——这是官方明确提示的“潜在风险”并非危言耸听。二、断言宏与 NULL为什么EXPECT_EQ(NULL, ptr)可行而EXPECT_NE(NULL, ptr)不可行2.1 首选nullptr而非NULLFAQ 明确指出EXPECT_NE(nullptr, ptr)与ASSERT_NE(nullptr, ptr)完全可用且是代码风格指南中推荐写法因为nullptr没有NULL的模板类型推导问题——这正是NULL无法直接用于EXPECT_NE的根源。2.2 模板元编程的成本权衡EXPECT_XX()/ASSERT_XX()系列宏要支持NULL作为参数需要引入非平凡的模板元编程技巧。官方只在需求最强烈的地方实现EXPECT_EQ(a, b)语义上第一个参数是期望值、第二个是实际值EXPECT_EQ(NULL, some_expression)的需求被多次提出因此实现了EXPECT_NE(NULL, ptr)的需求没那么强——断言失败时你本来就已知道ptr必为NULL此时打印ptr不增加任何信息写EXPECT_TRUE(ptr ! NULL)效果相同若支持EXPECT_NE(NULL, ptr)为保持一致性还须支持EXPECT_NE(ptr, NULL)EXPECT_NE没有参数顺序约定等于模板技巧要用两次维护成本翻倍收益不值得。2.3 更统一的出路matcher 语法随着 gMock matcher 库的成长官方正鼓励测试统一改用EXPECT_THAT(value, matcher)语法。matcher 的显著优势是可组合性matcher 之间可以自由组合成新 matcher而EXPECT_NE这类宏难以组合。TEN-framework 的 smoke 测试大量使用ASSERT_EQ(rc, true)、check_status_code、check_detail_with_string这类封装好的断言见 basic_hello_world_1.cc其本质正是基于 googletest 断言宏的二次封装适合在项目中沉淀公共断言工具。三、测试同一接口的多个实现typed tests 还是 value-parameterized tests两者都能完成“验证接口的所有实现满足共同需求”的任务选哪个取决于你的具体场景。FAQ 给出了五条实用判断准则考量维度Typed TestsValue-parameterized Tests实例创建方式各实现可用同一种方式创建如都有公共默认构造函数可写new TypeParam或工厂函数形式一致如CreateInstanceTypeParam()时更易写各实现需要不同代码模式创建如new Foovsnew Bar(5)时更易写可写工厂函数包装器把函数指针作为参数传入失败定位失败输出包含类型名能快速定位是哪个实现出问题只能靠迭代序号推断是哪个实现不够直观编译错误模板化代码导致编译错误更难消化编译错误相对友好接口约束必须确保测试针对接口类型而非具体类型即保证implicit_castMyInterface*(my_concrete_impl)成立这方面不易犯错FAQ 的最终建议很朴素两种都试试实践才是掌握二者细微差别的最好方式。需要留意的是typed tests 一旦写错模板化代码的编译错误信息会比较难读而 value-parameterized tests 的失败定位需要借助迭代序号。四、死亡测试Death Test专题死亡测试用于验证“某段代码会按预期崩溃/终止”是 googletest 中最复杂也最易踩坑的机制。FAQ 用了多个条目专门讲解。4.1 为什么死亡测试变慢了2008 年 8 月起由于线程化日志成为默认配置默认死亡测试风格从fast切换为threadsafe导致大量死亡测试变慢。这是必要代价。官方指引参考 Fixing Failing Death Tests仓库内对应死亡测试章节。4.2 死亡测试修改的状态为何“丢失”EXPECT_DEATH等死亡断言在子进程中执行以保证预期崩溃不会杀死测试程序父进程。因此子进程产生的任何内存副作用只在各自子进程内可见父进程完全观察不到——可以理解为它们运行在一个“平行宇宙”。FAQ 特别提醒若使用 gMock仓库内位于 third_party/googlemock死亡测试语句中调用的 mock 方法父进程会认为从未发生过因此应把EXPECT_CALL语句移进EXPECT_DEATH宏内部。4.3 死亡测试挂起或段错误googletest 的死亡测试在子进程中运行机制相当微妙。FAQ 给出修复路线首要排查消除EXPECT_DEATH()之外创建的线程。死亡测试不喜欢父进程存在多线程可用 mock 或 fake 对象替代真实对象若所用库在进入main()之前就创建了线程则把尽可能多的活动移进EXPECT_DEATH()极端情况全部移入或反过来尽可能少留也可尝试将死亡测试风格设为threadsafe更安全但更慢线程安全的死亡测试会在子进程中从头重跑测试程序因此必须保证程序可并排运行自身、且行为确定归根结底是并发编程问题确保程序无竞态、无死锁没有银弹。4.4 已 join 的线程仍触发 ASSERT_DEATH 报错在旧 Linux pthread 库下一旦从单线程跨入多线程就“回不去”首次创建线程时额外产生一个 manager 线程得到 3 个而非 2 个线程之后线程 join 回主线程计数减 1但 manager 线程永不消亡仍剩 2 个线程因此无法安全运行死亡测试。新的 NPTL 线程库没有此问题不创建 manager 线程但若无法控制测试运行机器就不应依赖这一点。4.5 为什么包含 ASSERT_DEATH 的整个测试用例必须命名为*DeathTestgoogletest 不会交错执行不同测试用例的测试它先跑完一个用例的所有测试再跑下一个因为需要在用例首测试前 set-up、结束后 tear-down。若按测试名而非用例名决定执行顺序就会产生矛盾场景TEST_F(FooTest, AbcDeathTest) { ... } TEST_F(FooTest, Uvw) { ... } TEST_F(BarTest, DefDeathTest) { ... } TEST_F(BarTest, Xyz) { ... }FooTest.AbcDeathTest须先于BarTest.Xyz同时BarTest.DefDeathTest又须先于FooTest.Uvw而不同用例不允许交错执行两者矛盾。这是 googletest 要求整个用例命名为*DeathTest的根本原因。4.6 不想把整个用例命名为 DeathTest 怎么办可以拆分用例名称清晰表达相关性class FooTest : public ::testing::Test { ... }; TEST_F(FooTest, Abc) { ... } TEST_F(FooTest, Def) { ... } using FooDeathTest FooTest; TEST_F(FooDeathTest, Uvw) { ... EXPECT_DEATH(...) ... } TEST_F(FooDeathTest, Xyz) { ... ASSERT_DEATH(...) ... }4.7 ASSERT_DEATH 的 statement 参数可以是什么ASSERT_DEATH(statement, regex)及任何死亡断言宏的statement只要在当前上下文合法即可可以是简单函数调用引用全局/局部变量的复杂表达式复合语句compound statement。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); }仓库内的 gtest 自带测试 gtest-death-test_test.cc 包含更多相关示例可供研读。4.8 成功时看不到子进程 LOG 消息打印EXPECT_DEATH()内语句产生的 LOG 会干扰父进程日志中真实问题的检索因此 googletest 只在死亡测试失败时打印这些 LOG。临时变通办法故意破坏死亡测试例如改掉期望匹配的正则使其失败以观察日志——FAQ 承认这是 hack并计划在 fork-and-exec 风格死亡测试落地后给出更永久的方案。五、协议缓冲区Protobuf断言ProtocolMessageEquals已弃用ProtocolMessageEquals与ProtocolMessageEquiv现已弃用应改用EqualsProto等。弃用的旧 matcher 对无效的 protocol buffer 定义更不宽容如果foo.proto未完整限定所引用消息的类型如写了messageBar而应为messageblah.Bar运行时会出现类似错误... descriptor.cc:...] Invalid proto descriptor for file path/to/foo.proto: ... descriptor.cc:...] blah.MyMessage.my_field: .Bar is not defined.看到这类报错说明.proto文件本身有缺陷需要把类型写成完全限定名。新定义只是恰好暴露了你的 bug。六、常见编译错误的成因与修复6.1EXPECT_EQ(htonl(blah), blah_blah)在 opt 模式下的诡异编译错误这不是 googletest 的 bug而是htonl()的问题按man htonl它是函数可作为函数指针使用但在 opt 模式下htonl()被定义为宏且该宏使用 gcc 扩展、并非标准 C存在临时性局限——它阻止你写Foosizeof(htonl(x))()这类带整型参数的模板。而EXPECT_EQ(a, b)的实现恰好在模板参数内使用sizeof(... a ...)因此 opt 模式下a含htonl()调用时无法编译。由于解决方案必须在各平台不同编译器下都成立难以让EXPECT_EQ绕过此缺陷。官方建议改用ghtonl()htons()对应ghtons()并记得在使用处把//util/endian加入 BUILD 依赖——它只含一个头文件不会撑大二进制。6.2 类体内定义的静态 const 成员出现 “undefined reference”在类体内这样写只是声明// foo.h class Foo { ... static const int kBar 100; };还必须在foo.cc中于类体外定义const int Foo::kBar; // 无需初始化器否则代码属于非法 C可能以意外方式损坏。尤其是用于 googletest 比较断言EXPECT_EQ等时会产生 “undefined reference” 链接错误。FAQ 提醒“以前能跑”不代表合法只是运气好。6.3 “void value not ignored as it ought to be”这通常意味着你在不返回 void 的函数里使用了ASSERT_*()。由于构建系统禁用了异常ASSERT_*()只能在 void 函数中使用详见 advanced.md 的 Assertion Placement 章节。6.4 构造函数或析构函数不能返回值为了支持向断言流式写入消息的语法ASSERT_EQ(1, Foo()) blah blah foo;googletest 不得不放弃在构造函数和析构函数中使用ASSERT*和FAIL*EXPECT*和ADD_FAILURE*不受影响。变通方案把构造函数/析构函数内容移到私有 void 成员函数中或改用EXPECT_*()。参见 advanced.md 的 Assertion Placement 章节。6.5 “no match for operator”若在断言中使用自定义类型FooType必须定义std::ostream operator(std::ostream, const FooType)以便打印其值。若FooType声明在命名空间中运算符也必须定义在同一命名空间。6.6ASSERT_PRED*的 “no matching function to call”谓词函数若为重载函数或模板编译器无法确定选哪个版本。ASSERT_PRED_FORMAT*/EXPECT_PRED_FORMAT*没有此问题。修复方式有二改用(ASSERT|EXPECT)_PRED_FORMAT*附带更好的失败消息显式指定版本例如EXPECT_PRED1(static_castbool (*)(int)(IsPositive), 5); // 明确选择 int 版本模板谓词可显式实例化ASSERT_PRED1(IsNegativeint, -5);多参数模板函数需注意宏参数计数问题——ASSERT_PRED2(GreaterThanint, int, 5, 0)会被预处理器认为传了 4 个参数必须用括号包住谓词ASSERT_PRED2((GreaterThanint, int), 5, 0);6.7 忽略RUN_ALL_TESTS()返回值把return RUN_ALL_TESTS();写成RUN_ALL_TESTS();是错误且危险的测试服务需要该返回值判断测试是否通过main()忽略它会导致即便断言失败测试也被视为成功。googletest 已在 gcc 下禁止忽略其返回值违反即编译报错修复就是确保它作为main()的返回值。TEN-framework 的 smoke 测试入口正是这样做的——gtest_main.cc 中GTEST_API_ int main(int argc, char **argv) { printf(Running main() from %s\n, __FILE__); testing::InitGoogleTest(argc, argv); // Add the environment to Google Test. ::testing::AddGlobalTestEnvironment(new GlobalTestEnvironment); return RUN_ALL_TESTS(); }可以看到仓库还在RUN_ALL_TESTS()之前通过AddGlobalTestEnvironment注册了一个全局环境GlobalTestEnvironment在SetUp()中创建 fake app 线程并等待其完成配置、在TearDown()中关闭并回收 app 线程——这是把 googletest 全局环境机制用于集成测试场景的典型范例。6.8TEST_F(FooTest, Bar)报 “no matching function for call to FooTest::FooTest()”googletest 需要能创建 fixture 对象因此 fixture 类必须存在默认构造函数通常编译器会自动生成。需要手写的情况显式声明了非默认构造函数如DISALLOW_EVIL_CONSTRUCTORS()的副作用时须补一个默认构造函数哪怕是空的fixture 含 const 非静态数据成员时必须在默认构造函数的初始化列表中初始化该 const 成员早期 gcc 不强制属于已在 gcc 4 修复的 bug。七、Test Fixture 的正确用法7.1 能否从一个 fixture 派生另一个——可以每个 test fixture 有同名且唯一的 test case。多个 test case 想共享同一或相近的fixture 时把共享逻辑放进基类 fixture再为每个 test case 派生专用 fixture用TEST_F()编写测试。典型结构// 定义基类 fixture class BaseTest : public ::testing::Test { protected: ... }; // 从 BaseTest 派生 FooTest class FooTest : public BaseTest { protected: void SetUp() override { BaseTest::SetUp(); // 先设置基类 fixture ... additional set-up work ... } void TearDown() override { ... clean-up work for FooTest ... BaseTest::TearDown(); // 记得在清理完 FooTest 后再拆卸基类 } ... functions and variables for FooTest ... }; // 使用 FooTest 的测试 TEST_F(FooTest, Bar) { ... } TEST_F(FooTest, Baz) { ... }必要时还可以从派生 fixture 继续派生googletest 对继承深度没有限制。完整示例见仓库内的 sample5_unittest.cc。7.2 构造函数/析构函数 还是 SetUp()/TearDown()关键前提googletest不会跨测试复用同一个 fixture 对象。对每个TEST_F它都会创建全新的fixture 对象、立即调用SetUp()、运行测试体、调用TearDown()然后销毁对象。构造函数/析构函数通常更受青睐因为在构造函数中初始化成员变量时可将其声明为const防止意外改动、使测试更显然正确子类化 fixture 时子类构造函数保证先调用基类构造函数、子类析构函数保证后调用基类析构函数而SetUp()/TearDown()下子类可能忘记调用基类版本或在错误时机调用。少数情况下仍需SetUp()/TearDown()构造函数体内无法使用ASSERT_xx宏若 set-up 操作可能产生致命失败应阻止测试继续须用CHECK宏或改用SetUp()tear-down 可能抛异常时必须用TearDown()——析构函数中抛异常是未定义行为通常直接杀死程序注意 STL 等标准库在启用异常时可能抛出想写可移植测试应优先TearDown()googletest 团队正考虑在启用异常的平台Windows、Mac OS、Linux 客户端让断言宏直接抛出届时不再需要用户向调用方传播失败——因此若代码可能运行于这类平台不要在析构函数中使用断言构造函数/析构函数中对本对象无法做虚函数调用声明为 virtual 的方法也会被静态绑定若需调用将被派生类覆写的方法必须用SetUp()/TearDown()。7.3SetUp()为什么没被调用C 区分大小写。你是不是写成了Setup()同理把SetUpTestCase()写成SetupTestCase()也会静默失效。7.4 多个 test case 共享 fixture 逻辑必须逐个定义新 fixture 类吗——不必与其写class FooTest : public BaseTest {}; TEST_F(FooTest, Abc) { ... } TEST_F(FooTest, Def) { ... } class BarTest : public BaseTest {}; TEST_F(BarTest, Abc) { ... } TEST_F(BarTest, Def) { ... }不如直接typedeftypedef BaseTest FooTest; TEST_F(FooTest, Abc) { ... } TEST_F(FooTest, Def) { ... } typedef BaseTest BarTest; TEST_F(BarTest, Abc) { ... } TEST_F(BarTest, Def) { ... }7.5 为什么优先用 fixture 而非全局变量测试很可能需要改变全局变量状态副作用易从一个测试泄漏、污染其他测试fixture 让每个测试拥有全新但同名的变量集合测试彼此独立全局变量污染全局命名空间fixture 可通过子类化复用多个 test case 有共性的场景全局变量难以做到。7.6 同名的 TEST 方法可以出现在不同命名空间吗规则是同一 test case 的所有测试方法必须使用同一个 fixture 类。因此下面这种写法允许两者都用::testing::Testnamespace foo { TEST(CoolTest, DoSomething) { SUCCEED(); } } // namespace foo namespace bar { TEST(CoolTest, DoSomething) { SUCCEED(); } } // namespace bar而下面这种不允许会触发 googletest 运行时错误同一 test case 名使用了不同的 fixture 类namespace foo { class CoolTest : public ::testing::Test {}; TEST_F(CoolTest, DoSomething) { SUCCEED(); } } // namespace foo namespace bar { class CoolTest : public ::testing::Test {}; TEST_F(CoolTest, DoSomething) { SUCCEED(); } } // namespace bar八、输出、调试与测试管理的实用技巧8.1 在 Emacs 中直接跳到失败行googletest 的失败消息格式被 Emacs 及 acme、XCode 等众多 IDE 识别。若消息出现在 Emacs 的编译缓冲区中它可以直接点击跳转。8.2 测试输出被 LOG 淹没googletest 输出设计为简洁、人性化的报告测试自身产生的文本输出会与之混杂。解决方案很简单LOG 消息走 stderrgoogletest 输出走 stdout用重定向分离即可$ ./my_test gtest_output.txt8.3 代码如何检测自己是否运行在测试中——不建议在测试环境中嗅探并改变行为等于把测试专用逻辑泄漏进生产代码无法保证测试专用路径不会在生产中误运行还会引发 Heisenbug观测改变结果类问题googletest 不提供该能力。推荐做法是依赖注入测试与生产代码注入不同功能生产代码根本不链接测试逻辑。若实在万不得已可遵循“测试程序名以_test结尾”的约定在main()中嗅探argv[0]——FAQ 直言这是“可怕的 hack”。8.4 如何临时禁用某个测试给测试名加DISABLED_前缀即可排除其执行TEST(MyTest, DISABLED_ThatBrokenTest) { ... }这优于注释掉代码或#if 0因为被禁用的测试仍参与编译不会“腐烂”。需要运行被禁用的测试时用--gtest_also_run_disabled_tests标志调用测试程序。8.5 如何抑制 Windows 上的内存泄漏报告由于静态初始化的 googletest 单例需要在堆上分配Visual C 的内存泄漏检测器会在程序结束时报告泄漏。规避方式是使用_CrtMemCheckpoint与_CrtMemDumpAllObjectsSince不报告任何静态初始化的堆对象。九、TEN-framework 中的 googletest 落地实践结合仓库源码可以看到 googletest 在 TEN-framework 中的真实用法依赖声明smoke 测试的 BUILD.gn 通过public_deps引入//third_party/googlemock与//third_party/googletestgoogletest 以仓库内 third_party 源码形式参与构建测试组织tests/ten_runtime/smoke 下按功能目录basic、command、extension、engine、concurrent等组织大量.cc测试文件每个文件内部用匿名命名空间包裹测试辅助类、以TEST(BasicTest, HelloWorld1)形式组织用例真实断言用法basic_hello_world_1.cc 中TEST内创建 app 线程、建立 msgpack TCP 客户端、发送 start_graph 命令与自定义hello_world命令并用ten_test::check_status_code/check_detail_with_string校验结果——测试体与 fixture 机制的配合方式完全遵循本文所述规范用例名BasicTest无下划线、fixture 默认构造等自定义入口与全局环境gtest_main.cc 展示了在main()中调用testing::InitGoogleTest、AddGlobalTestEnvironment注册全局环境其SetUp/TearDown负责 fake app 生命周期、最终return RUN_ALL_TESTS()的标准范式与 FAQ 中关于RUN_ALL_TESTS()返回值的要求完全一致。结语googletest 的许多“怪癖”并非缺陷而是 C 语言规则与框架设计取舍的必然结果。理解命名保留规则、NULL的模板推导局限、死亡测试的子进程模型、fixture 的生命周期与继承机制以及各类编译错误的真实成因能让你的测试代码在编译器升级、googletest 版本迭代时依然稳健。若想进一步系统学习可继续阅读仓库内的 Primer.md 与 advanced.md并结合 Samples.md 中的示例逐一实践。赞分享人工智能AI Agent多模态语音AI 应用【免费下载链接】ten-frameworkOpen-source framework for conversational voice AI agents项目地址https://gitcode.com/TEN-framework/ten-framework点击查看免费下载相关推荐深入解析 googletest FAQ命名规则、死亡测试与断言宏陷阱实战指南深入解析 googletest FAQ命名规则、死亡测试与断言宏陷阱实战指南 googletestGoogleTest是业界广泛使用的 C 单元测试框数据库客户端桌面应用miniblink49 内嵌 Google Test V1.6 权威 FAQ 精解从命名规范到死亡测试的实战问答miniblink49 内嵌 Google Test V1.6 权威 FAQ 精解从命名规范到死亡测试的实战问答 本文以仓库 v8_7_5/testing/g前端桌面应用GoogleTest 官方 FAQ 深度解析从测试命名规则到 Death Test 陷阱的源码级答案GoogleTest 官方 FAQ 深度解析从测试命名规则到 Death Test 陷阱的源码级答案 本文以 GoogleTest 官方 FAQ docs/测试质量保障开发工具上一篇KubeSphere 镜像仓库访问的核心引擎深入解析 go-containerregistry remote 包下一篇华硕笔记本硬件控制的现代化解决方案G-Helper技术深度解析与实践指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表