十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

yaml-cpp 测试实践:Legacy gMock FAQ 全解——mock 编写、期望匹配与调试指南

yaml-cpp 测试实践:Legacy gMock FAQ 全解——mock 编写、期望匹配与调试指南 yaml-cpp 测试实践Legacy gMock FAQ 全解——mock 编写、期望匹配与调试指南【免费下载链接】yaml-cppA YAML parser and emitter in C项目地址: https://gitcode.com/GitHub_Trending/ya/yaml-cpp导读本文以 gMockGoogle Mock官方 FAQLegacy gMock FAQ为骨架系统梳理在 C 单元测试中使用 gMock 编写 mock 对象时最常遇到的问题方法未被 mock 掉、可变参数函数与 const 参数的处理、期望不匹配的调试、调用次数约束、新期望覆盖旧期望 的匹配规则、参数副作用动作的编写以及内存检查与编译期资源问题。同时结合本仓库yaml-cpp实际测试代码展示这些 FAQ 结论在真实项目中的落地形态——yaml-cpp 的解析器事件流测试正是依赖 gMock 的StrictMock、InSequence与EXPECT_CALL来逐事件校验 YAML 解析输出的。读完本文你将掌握如何判断并修复 mock 失效问题、如何让EXPECT_CALL与ON_CALL各司其职、如何用--gmock_verbose定位期望不匹配、如何用InSequence/WillOnce表达调用顺序、以及如何自定义 Action 处理 mock 参数。一、为什么 mock 方法总是调用到真实对象1. 方法必须是 virtualFAQ 的第一条结论简洁明确要让方法被 mock它必须是 virtual 的除非采用 high-perf 依赖注入技术。原因是MOCK_METHOD生成的 mock 类通过继承并 override 基类的虚函数来接管调用。如果基类方法不是 virtual派生类中同签名的方法会直接隐藏hide而非覆盖override基类实现导致多态分发失效调用仍落在真实对象上。在 yaml-cpp 中被 mock 的接口全部是纯虚函数。例如事件处理接口 eventhandler.h 中OnDocumentStart、OnScalar、OnSequenceStart、OnMapStart等均为virtual ... 0且~EventHandler()显式声明为virtual。测试侧的 mock 类 mock_event_handler.h 通过MOCK_METHODn逐一覆盖这些接口保证了Parser::HandleNextDocument分发事件时调用的是 mock 实现。2. 非虚方法怎么办high-perf 依赖注入如果确需 mock 非虚方法FAQ 指向 cookbook 的 Mocking Non-virtual Methods 配方让 mock 类与真实类不共享基类只保持相同的方法签名然后在编译期通过模板参数选择具体类型——// 真实类无任何虚成员 class ConcretePacketStream { public: void AppendPacket(Packet* new_packet); const Packet* GetPacket(size_t packet_number) const; size_t NumberOfPackets() const; }; // mock 类不继承任何类仅定义同签名方法 class MockPacketStream { public: MOCK_METHOD(const Packet*, GetPacket, (size_t packet_number), (const)); MOCK_METHOD(size_t, NumberOfPackets, (), (const)); }; // 生产代码与测试代码通过模板类型参数切换 template class PacketStream void CreateConnection(PacketStream* stream) { ... } template class PacketStream class PacketReader { public: void ReadPackets(PacketStream* stream, size_t packet_num); };生产代码实例化PacketReaderConcretePacketStream测试代码实例化PacketReaderMockPacketStream并对其设置期望。由于两者无继承关系选择必须在编译期模板完成而非运行期虚函数。二、mock 不了的方法可变参数函数与顶层 const 参数1. 可变参数函数variadic function无法直接 mockFAQ 明确指出gMock无法直接 mock 一个带省略号...参数的函数。根本原因在于 mock 对象在编译期无法获知可变参数的个数与类型——只有基类作者才知道调用协议而 gMock 不可能读心。可行的替代方案是为该函数提供重载版本把可能的参数组合显式枚举出来。FAQ 同时提醒省略号参数继承自 C并非真正的 C 特性传参含构造/析构函数的对象时不安全应尽量在 C 中避免使用。2. MSVC 警告 C4301 / C4373 与顶层 const 参数当 mock 方法带顶层const参数如const int i时MSVC 可能报出以下警告warning C4301: MockFoo::Bar: overriding virtual function only differs from Foo::Bar by const/volatile qualifier # Visual C 2008 SP1 之后变为 warning C4373: MockFoo::Bar: virtual function overrides Foo::Bar, previous versions of the compiler did not override when parameters only differed by const/volatile qualifiers这是 MSVC 的已知缺陷同样的代码用 gcc 编译并无问题。原因在于 C 语言规则函数声明中的顶层const修饰符会被忽略因此以下两个声明等价virtual void Bar(int i) 0; virtual void Bar(const int i) 0; // 顶层 const 无意义结论在Foo和MockFoo中都删掉顶层const即可规避该警告。但要注意区分顶层 const与所指对象的 const。若参数是指针或引用const修饰指针所指对象pointee/referee依然有意义void Bar(int* p); // p 与 *p 均非 const void Bar(const int* p); // p 非 const但 *p 是 const这两者并不等价不可混为一谈。三、期望不匹配从 verbose 日志到默认动作1. 用--gmock_verboseinfo定位问题当 gMock 报告期望未满足却看不出原因时FAQ 建议以--gmock_verboseinfo运行测试。该标志会让 gMock 打印每次 mock 函数调用的跟踪信息通过研读调用轨迹即可判断期望为何未命中。若看到如下提示The mock function has no default action set, and its return type has no default value set.则应尝试添加默认动作ON_CALL/DefaultValueT。FAQ 还提示一个已知问题对没有默认动作的 mock 的意外调用不会打印实际参数与期望参数的详细比对因此设置默认动作也有助于获得更详细的失败信息。2. 程序崩溃时ScopedMockLog刷屏当测试崩溃时失败信号处理器会输出大量信息堆栈、地址映射等多线程深堆栈场景下更是成倍放大。ScopedMockLog拦截到这些不匹配任何期望的日志时会逐条报错。FAQ 给出的建议是要么学会忽略这些错误要么把期望写得更健壮例如using ::testing::AnyNumber; using ::testing::Not; ... // 忽略任何不是我们产生的日志。 EXPECT_CALL(log, Log(_, Not(EndsWith(/my_file.cc)), _)) .Times(AnyNumber());3. 如何断言绝不被调用要断言某个函数从未被调用使用.Times(0)using ::testing::_; ... EXPECT_CALL(foo, Bar(_)) .Times(0);结合 yaml-cpp 的测试代码可见Times之外的默认期望是允许任意次数调用只有显式写Times(0)或Times(AnyNumber())才是对该调用次数约束的明确声明。四、理解 gMock 的匹配哲学顺序、重复报告与新期望覆盖旧期望1. 两次报告同样的期望状态并不冗余FAQ 解释gMock 每次检测到失败都会打印相关信息mock 参数、相关期望的状态等。若两次失败之间某个期望的状态没有变化就会看到同样的状态描述出现两次。但这两次对应的是不同的时间点——状态相同这一事实本身就是有用的调试信息因此并非冗余。2. 为什么新期望覆盖旧期望gMock 默认按从后往前的顺序搜索期望与ON_CALL。这样设计是为了支持一个非常有用的模式先在 mock 构造或 fixture 的SetUp()阶段为常见情形设定默认行为再在具体测试中用更精确的规则覆盖。若改为从前向后搜索该模式将无法实现。FAQ 以一个反面示例说明有人试图用两条倒序的WillOnce表达第一次返回 1第二次返回 2却抱怨要倒着写。正确的做法有两种方式一用InSequence保证顺序期望按自然顺序书写using ::testing::Return; ... { InSequence s; EXPECT_CALL(foo, Bar()) .WillOnce(Return(1)) .RetiresOnSaturation(); EXPECT_CALL(foo, Bar()) .WillOnce(Return(2)) .RetiresOnSaturation(); }方式二把动作序列放进同一个期望using ::testing::Return; ... EXPECT_CALL(foo, Bar()) .WillOnce(Return(1)) .WillOnce(Return(2)) .RetiresOnSaturation();FAQ 强调gMock 的基本哲学是默认情况下期望不要求按任何特定顺序匹配若要强制顺序必须显式声明——因为测试很容易被无意中过度约束over-specify。yaml-cpp 的解析器事件测试正是这一哲学的典型应用。handler_test.h 在 fixture 中声明了InSequence sequence;和StrictMockMockEventHandler handler;于是 handler_test.cpp 中一系列EXPECT_CALL(handler, OnDocumentStart(_))、OnMapStart(...)、OnScalar(...)、OnMapEnd()、OnDocumentEnd()必须按 YAML 事件流的严格顺序逐一命中——这正是解析器输出顺序性的直接验证。3. 为什么有ON_CALL默认行为还会收到警告FAQ 明确回答宁可啰嗦不可放过being neat vs being safe, we lean toward the latter。ON_CALL写在 mock 构造或SetUp()中是为了提供默认行为它不代表调用被期望。若没有对应的EXPECT_CALL而方法仍被调用很可能就是 bug悄悄放行会让缺陷悄无声息地溜过去。如果你确信这些调用是合理的应改用EXPECT_CALL并声明调用次数using ::testing::_; ... EXPECT_CALL(foo, Bar(_)) .WillRepeatedly(...); // 而不是 ON_CALL(...).WillByDefault(...)这等于告诉 gMock我确实期望这些调用从而抑制警告。此外可用--gmock_verbose控制输出量可选值包括info、warning、error——调试输出太吵时降低 verbosity 即可详见 Cheat Sheet 的 Flags 表。五、对 mock 参数执行动作DeleteArg、Invoke 与自定义 Action1. 在 action 中删除参数若 mock 函数接收指针参数且希望在动作中删除它可使用testing::DeleteArgN()删除第 N 个从 0 开始参数using ::testing::_; ... MOCK_METHOD(void, Bar, (X* x, const Y y)); ... EXPECT_CALL(mock_foo_, Bar(_, _)) .WillOnce(testing::DeleteArg0());2. 对参数执行任意动作gMock 未直接支持的动作可通过以下三种途径实现用MakeAction()构造单类型动作用MakePolymorphicAction()构造多态动作适用于Return(value)这类可用于多种函数类型的动作写一个 stub 函数并用Invoke()调用它。using ::testing::_; using ::testing::Invoke; ... MOCK_METHOD(void, Bar, (X* p)); ... EXPECT_CALL(mock_foo_, Bar(_)) .WillOnce(Invoke(MyAction(...)));3. 自定义动作选型Invoke 还是 ActionInterfaceFAQ 给出的选型建议动作只适用于某一种函数类型时用Invoke()更简单动作要用于多种函数类型如Return(*value*)时MakePolymorphicAction()最省事需要精确控制动作可用的函数类型范围时实现ActionInterface接口参见gmock-actions.h中Return()的实现。4.SetArgPointee()与 conflicting return type specified在WillOnce()中使用SetArgPointee()时gcc 报 conflicting return type specified 是因为SetArgPointee()只描述副作用写回指针所指对象没有提供返回值gMock 不知道 mock 方法应该返回什么。需要用DoAll()把SetArgPointee()与一个Return()串联后者为被 mock 的 API 提供合适返回值。完整示例见 Mocking Side Effects 配方。六、面向对象设计层面的 FAQstatic 函数、复杂动作与内存1. 能否 mock static/全局函数FAQ 的回答是可以但需要改造。需要 mock 静态函数通常是模块耦合过紧的信号——此时更值得做的是定义一个小的接口让调用通过该接口进行从而可被轻松 mock。这最初会有些工作量但通常很快回本。2. 我的 mock 要干很多复杂的事gMock 太难用了FAQ 用一句反问点明本质这是用错了工具。它区分了两种测试范式状态型测试state-based testing执行代码后断言返回值或系统最终状态交互型测试interaction-based testingmock 对象验证自己是否被以正确的方式调用并在错误出现的第一时间报告从而精确定位出错上下文——这通常比状态型测试更高效经济。若你只是在做状态型测试、用测试替身模拟真实对象fake假实现往往比 mock 更合适mock 的强项在于交互验证而非执行复杂动作。感到mock 很痛苦时要么是工具选错要么是问题本身就问错了。3. Uninteresting function call encountered 警告需要恐慌吗FAQ 明确回答不需要这只是 FYI供参考信息。它的含义是某个 mock 函数没有设置任何期望按 gMock 规则即你不关心它的调用可以任意次数调用而它确实被调用了——这并不违规。但也存在一种可能你本意是禁止调用却忘了写EXPECT_CALL(foo, Bar()).Times(0)。因此看到该消息时若你认为不应存在无趣调用就应排查——gMock 会dump 堆栈跟踪据此可以定位是哪个 mock 函数、如何被调用的。4. 堆检查heapcheck失败虚析构函数mock 对象导致堆检查失败、真实对象却正常时FAQ 首先提醒检查被 mock 的基类是否有虚析构函数。当通过基类指针delete派生类对象而基类析构非虚时派生类析构不会执行class Base { public: ~Base() { ... } // 非虚——应改为 virtual }; class Derived : public Base { private: std::string value_; }; Base* p new Derived; delete p; // 只调用 ~Base()~Derived() 不执行value_ 泄漏将~Base()改为 virtual 后delete p会正确调用~Derived()。在 yaml-cpp 中eventhandler.h 的virtual ~EventHandler() default;正是这一原则的实践——被 mock 的接口类必须保证多态删除的安全性。5. 巨型 mock 类导致 MSVC 内存耗尽FAQ 观察到启用/clrCLR 公共语言运行时支持标志时Visual C 编译 mock 类会消耗5~6 倍的内存。建议在编译原生 C mock 时避免使用/clr。七、FAQ 结论在 yaml-cpp 测试中的实际落地为了让上述 FAQ 的每条结论都能落到可验证的代码上这里汇总 yaml-cpp 仓库中与 gMock 直接相关的关键路径FAQ 主题yaml-cpp 仓库中的对应证据方法必须 virtualeventhandler.h 中事件接口全部为纯虚函数虚析构的必要性eventhandler.h 中virtual ~EventHandler() default;MOCK_METHOD 定义 mockmock_event_handler.h 用MOCK_METHOD0/1/2/4覆盖全部事件回调InSequence / StrictMock 顺序期望handler_test.h 在 fixture 中声明InSequence sequence;与StrictMockMockEventHandler handler;EXPECT_CALL 逐事件断言handler_test.cpp 按文档-映射-标量-结束事件流顺序设置期望NiceMock 忽略无趣调用handler_test.h 中IgnoreParse使用NiceMockMockEventHandler容忍无趣调用gmock 头文件与链接test/CMakeLists.txt 将 googletest-1.16.0 作为测试依赖引入从源码结构看yaml-cpp 的解析器事件测试把Parser::HandleNextDocument的事件回调全部交给StrictMockMockEventHandler配合 fixture 级别的InSequence实现了对 YAML 解析事件序列的全序验证——这正是交互型测试在解析器正确性验证上的典型用法不检查最终 Node 状态而是逐事件比对回调序列是否符合 YAML 规范。八、速查gMock 常用调试标志与动作综合本文涉及的 gMock 命令与选项汇总如下完整列表见 gmock_cheat_sheet.md选项 / 用法作用--gmock_verboseinfo打印每次 mock 调用的跟踪用于排查期望未满足--gmock_verbosewarning/error降低输出级别调试时减少噪音--gmock_catch_leaked_mocks0不把泄漏的 mock 对象当作测试失败报告EXPECT_CALL(...).Times(0)断言函数绝不被调用EXPECT_CALL(...).Times(AnyNumber())允许任意次数调用并抑制无趣调用警告ON_CALL(...).WillByDefault(...)设置默认行为不构成期望InSequence s;让块内期望按书写顺序匹配WillOnce(Return(1)).WillOnce(Return(2))同一期望内按序返回不同值testing::DeleteArgN()在动作中删除第 N 个指针参数Invoke(...)/MakePolymorphicAction(...)/ActionInterface自定义动作的三种实现途径DoAll(SetArgPointee(...), Return(...))同时产生副作用并提供返回值FAQ 的核心思想可以总结为一句话gMock 的期望EXPECT_CALL与默认行为ON_CALL是两种不同语义的机制——前者声明必须发生的事后者提供没人在意时的兜底行为理解并善用这一区分绝大多数 FAQ 中的困惑都能迎刃而解。【免费下载链接】yaml-cppA YAML parser and emitter in C项目地址: https://gitcode.com/GitHub_Trending/ya/yaml-cpp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表