ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Google Mock v1.7 常见问题深度指南:模板错误诊断、Matcher API 迁移与 Mock 测试实战(miniblink49 内置 gmock 解析)

Google Mock v1.7 常见问题深度指南:模板错误诊断、Matcher API 迁移与 Mock 测试实战(miniblink49 内置 gmock 解析) Google Mock v1.7 常见问题深度指南模板错误诊断、Matcher API 迁移与 Mock 测试实战miniblink49 内置 gmock 解析【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49本文以 miniblink49 仓库内置的 Google Mockgmockv1.7 官方 FAQ 文档v8_6_7/testing/gmock/docs/v1_7/FrequentlyAskedQuestions.md为主体系统梳理使用 gmock 时最容易踩坑的二十余个高频问题从mock 方法为何调到了真实对象、Matcher 新老 API 迁移到 GCC/ MSVC 编译器报错解读、堆检查失败与期望覆盖规则。文中每个结论均结合仓库内 gmock 源码 与 测试用例 给出依据读完你不仅能避开这些坑还能理解 gmock 底层设计哲学让 mock 测试从能用走向用好。在 miniblink49 中Google Mock 以 v8 引擎测试基础设施的形式随 v8_6_7/testing/gmock/ 目录一并提供包含完整的头文件include/gmock/、实现src/、构建脚本make/、msvc/、scripts/以及约 20 个测试文件test/。这份 FAQ 正是 gmock 作者对用户反馈的沉淀本文按原文档脉络逐条展开并补充源码级证据。一、Mock 方法为何调用了真实对象——必须 virtual症状对 mock 对象调用某个方法时执行的是真实对象的方法。原因gmock 通过继承并在子类中重写基类方法来实现 mock而 C 中只有virtual成员函数才能被正确重写overriding。如果一个方法没有virtual修饰基类指针/引用调用它时走的仍是基类实现mock 版本永远不会被触发。解决将被 mock 的方法声明为virtual如果基类方法不是 virtual 且无法修改可以采用文档提到的high-perf dependency injection technique高性能依赖注入技巧——为被测代码引入一个轻量接口把依赖的函数调用收敛到接口上再 mock 这个接口。该技巧的详细配方见仓库内 CookBook 的 Mocking Nonvirtual Methods 章节。延伸这也是 gmock 设计上的第一原则——能被 mock 的前提是接口可替换。从仓库源码看所有 mock 类都通过MOCK_METHODn宏展开为对基类虚函数的 override见 gmock-generated-function-mockers.h因此虚函数表vtable机制是 gmock 运转的根基。二、升级 gmock 后自定义 Matcher 不再编译Matches() 到 MatchAndExplain() 的迁移背景gmock 1.4.0 之后为了让 matcher 能高效生成更丰富的信息化错误消息官方对 matcher 的扩展 API 做了不兼容调整。如果你是通过实现MatcherInterface或使用MakePolymorphicMatcher()编写自定义 matcher旧代码将无法编译而用MATCHER*宏族定义的 matcher 不受影响。迁移的核心只有一句话把Matches()改名为MatchAndExplain()并新增一个MatchResultListener*参数。2.1 实现 MatcherInterface 的迁移旧写法升级后无法编译using ::testing::MatcherInterface; ... class MyWonderfulMatcher : public MatcherInterfaceMyType { public: ... virtual bool Matches(MyType value) const { // Returns true if value matches. return value.GetFoo() 5; } ... };新写法与最新 gmock 兼容using ::testing::MatcherInterface; using ::testing::MatchResultListener; ... class MyWonderfulMatcher : public MatcherInterfaceMyType { public: ... virtual bool MatchAndExplain(MyType value, MatchResultListener* listener) const { // Returns true if value matches. return value.GetFoo() 5; } ... };即重命名Matches()为MatchAndExplain()并添加类型为MatchResultListener*的第二个参数。2.2 原先用 ExplainMatchResultTo() 增强消息时的迁移如果旧 matcher 还重写了ExplainMatchResultTo()来输出辅助信息using ::testing::MatcherInterface; ... class MyWonderfulMatcher : public MatcherInterfaceMyType { public: ... virtual bool Matches(MyType value) const { return value.GetFoo() 5; } virtual void ExplainMatchResultTo(MyType value, ::std::ostream* os) const { // Prints some helpful information to os to help // a user understand why value matches (or doesnt match). *os the Foo property is value.GetFoo(); } ... };迁移方式把ExplainMatchResultTo()的逻辑搬进MatchAndExplain()原来写向::std::ostream*的内容改写到MatchResultListener*using ::testing::MatcherInterface; using ::testing::MatchResultListener; ... class MyWonderfulMatcher : public MatcherInterfaceMyType { public: ... virtual bool MatchAndExplain(MyType value, MatchResultListener* listener) const { // Returns true if value matches. *listener the Foo property is value.GetFoo(); return value.GetFoo() 5; } ... };2.3 基于 MakePolymorphicMatcher() 的迁移用MakePolymorphicMatcher()定义多态 matcher 的旧代码using ::testing::MakePolymorphicMatcher; ... class MyGreatMatcher { public: ... bool Matches(MyType value) const { return value.GetBar() 42; } ... }; ... MakePolymorphicMatcher(MyGreatMatcher()) ...同样只需改名并加参数using ::testing::MakePolymorphicMatcher; using ::testing::MatchResultListener; ... class MyGreatMatcher { public: ... bool MatchAndExplain(MyType value, MatchResultListener* listener) const { return value.GetBar() 42; } ... }; ... MakePolymorphicMatcher(MyGreatMatcher()) ...2.4 多态 matcher 使用 ExplainMatchResultTo() 时的迁移旧代码把解释逻辑放在独立的ExplainMatchResultTo(const MyGreatMatcher, MyType, ::std::ostream*)自由函数中using ::testing::MakePolymorphicMatcher; ... class MyGreatMatcher { public: ... bool Matches(MyType value) const { return value.GetBar() 42; } ... }; void ExplainMatchResultTo(const MyGreatMatcher matcher, MyType value, ::std::ostream* os) { *os the Bar property is value.GetBar(); } ... MakePolymorphicMatcher(MyGreatMatcher()) ...迁移后把解释逻辑移入MatchAndExplain()并用listener输出using ::testing::MakePolymorphicMatcher; using ::testing::MatchResultListener; ... class MyGreatMatcher { public: ... bool MatchAndExplain(MyType value, MatchResultListener* listener) const { *listener the Bar property is value.GetBar(); return value.GetBar() 42; } ... }; ... MakePolymorphicMatcher(MyGreatMatcher()) ...2.5 源码层面理解 MatchResultListener 的设计动机仓库源码 gmock-matchers.h 明确定义了MatchResultListener它是一个抽象类重载了operator当底层 ostream 为 NULL 时写入操作是空操作。它提供两个关键方法IsInterested()返回底层流是否为非空matcher 可以据此避免在无人需要解释时生成昂贵的解释文本——这正是 1.4.0 之后引入该 API 想解决的性能问题stream()返回底层::std::ostream*。新的MatcherInterfaceT唯一需要实现的匹配方法是纯虚的MatchAndExplain(T x, MatchResultListener* listener)见 gmock-matchers.h。此外源码还提供了三种具体 listenerStringMatchResultListener把解释累积到std::stringstream供内部拼装失败消息用gmock-matchers.hDummyMatchResultListener丢弃所有解释用于只关心是否匹配的Matches()快速路径gmock-matchers.hStreamMatchResultListener把解释转发给指定 ostreamgmock-matchers.h。而MakePolymorphicMatcher()返回的PolymorphicMatcherImpl会在内部把 Impl 适配成每个具体类型的MonomorphicImpl并把MatchAndExplain委托给 Implgmock-matchers.h。因此无论你采用哪种方式写 matcher最终都会落在MatchAndExplain契约上。官方另外的 monomorphic/polymorphic matcher 编写配方可继续参考 CookBook 的 Writing New Monomorphic Matchers 与 Writing New Polymorphic Matchers 两节。三、用 gmock 就必须用 Google Test 吗——完全不必gmock 开箱即用地与 Google Test 协同工作但它很容易配置成与任意测试框架配合使用。核心做法是提供一个适配层gmock 对外部断言/失败机制的唯一依赖是一个用于报告失败的空操作函数你只需把该函数重定向到自己的测试框架的失败处理即可。具体配置步骤见 ForDummies 的 Using Google Mock with Any Testing Framework 一节。这一设计也印证了 gmock 的分层结构gmock.h只依赖一组可插拔的宏与函数src/中的实现如 gmock-spec-builders.cc并不强绑定 gtest 内部类型。四、Google Mock Doctor把可怕的模板错误翻译成人话症状gcc 抛出一屏难以理解的模板错误不知所措。方案使用 gmock 自带的Google Mock Doctor工具。它读取 stdin 中的 gcc 错误输出把 gmock 特有的编译错误官方称之为 diseases疾病翻译成可读的诊断说明。该工具在仓库中的实现位于 scripts/gmock_doctor.py其源码确认工具从 stdin 读取文本sys.stdin.read()见 gmock_doctor.py并内置了一张 gmock 常见符号表_COMMON_GMOCK_SYMBOLS用于识别错误上下文。安装配置别名alias gmdpath to googlemock/scripts/gmock_doctor.py在 miniblink49 仓库中对应路径为alias gmdv8_6_7/testing/gmock/scripts/gmock_doctor.py使用方式一管道喂入编译输出your-favorite-build-command your-test 21 | gmd例如make my_test 21 | gmd使用方式二交互式粘贴直接运行gmd然后把 gcc 的错误消息复制粘贴给它即可。五、能 mock 可变参数函数variadic function吗不能直接 mock。gmock 的 mock 机制要求编译期确定每个参数的数量与类型而带省略号...的可变参数函数恰恰无法在编译期获知这些信息——只有基类作者才知道调用协议mock 框架无法看穿他的大脑。可行的替代方案由用户自己提供该函数的重载版本overloads把可变参数协议显式化为若干固定签名再对这些重载进行 mock。官方态度省略号参数继承自 C并非真正的 C 特性它不安全且无法与带有构造/析构函数的参数类型配合使用。因此建议在 C 中尽可能避免使用可变参数。六、MSVC 报 C4301 / C4373 警告const 参数引起的假差异在 Visual C 2005 SP1 下编译如下代码class Foo { ... virtual void Bar(const int i) 0; }; class MockFoo : public Foo { ... MOCK_METHOD1(Bar, void(const int i)); };可能得到警告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 的 bug同样的代码在 gcc 下编译无碍。C 语言规则规定函数声明中的顶层const参数修饰会被忽略。也就是说class Foo { ... virtual void Bar(int i) 0; // int or const int? Makes no difference. };上面两个Bar声明完全等价——你甚至可以用int声明、用const int定义编译器仍认为它们是同一个函数。解决既然方法声明中参数加const没有意义官方建议在Foo和MockFoo中同时移除这个顶层 const即可绕过 VC 的 bug。重要区分以上讨论的只是顶层top-levelconst。如果参数是指针或引用那么被指对象pointee或被引用对象referee的 const 依然有意义以下两个声明并不等价void Bar(int* p); // Neither p nor *p is const. void Bar(const int* p); // p is not const, but *p is.七、巨型 mock 类导致 MSVC 编译内存耗尽gmock 发现当启用/clr公共语言运行时支持编译开关时VC 编译一个 mock 类会消耗约 5~6 倍的内存。建议编译原生 C mock 时避免使用/clr。如果你必须同时使用托管代码应把 mock 相关源文件拆出单独以纯原生方式编译。八、期望总是不满足用 --gmock_verboseinfo 看调用轨迹症状测试失败但搞不清 gmock 为什么认为期望未满足。方案带上--gmock_verboseinfo运行测试。该 flag 会让 gmock打印它收到的每一次 mock 函数调用的轨迹trace通过研读轨迹即可定位期望未满足的原因。补充说明gmock 的 verbose 级别共有三个——info、warning、error见 CheatSheet 的命令行 flag 表。日常调试用info获得最全信息嫌输出太吵时可以退回warning甚至error。verbose 级别同样可以在代码里通过::testing::FLAGS_gmock_verbose全局变量控制参考 CookBook。九、如何断言某个函数从未被调用使用.Times(0)EXPECT_CALL(foo, Bar(_)) .Times(0);这是最常见的负面期望写法与 gmock 的规则一致未显式设置期望的函数允许被任意次调用而一旦设置了.Times(0)任何一次调用都会立刻触发失败报告。十、同一期望被报告两次未满足是不是冗余输出不是。gmock 每检测到一次失败都会打印当时的完整相关信息mock 函数参数、相关期望的状态等。如果后续又检测到另一次失败它会再次打印同样的上下文。当两次失败之间某个期望的状态并未变化时你确实会看到相同的状态描述出现两次——但它们对应的是不同的时间点。两次状态恰好相同本身就是一条有价值的信息说明这段时间里该期望没有被触碰、也没被满足这正是诊断问题时需要关注的线索。十一、用 mock 对象时堆检查失败用真实对象却正常先检查你 mock 的那个类最好是纯接口有没有 virtual 析构函数只要从基类派生就必须确保基类析构函数是virtual否则会发生坏事情。看这个例子class Base { public: // Not virtual, but should be. ~Base() { ... } ... }; class Derived : public Base { public: ... private: std::string value_; }; ... Base* p new Derived; ... delete p; // Surprise! ~Base() will be called, but ~Derived() will not // - value_ is leaked.把~Base()改为 virtual 后delete p会正确触发~Derived()成员如value_得以释放堆检查器自然就满意了。十二、新期望覆盖旧期望规则太别扭——先理解 gmock 的匹配哲学用户常见的抱怨代码// foo.Bar() should be called twice, return 1 the first time, and return // 2 the second time. However, I have to write the expectations in the // reverse order. This sucks big time!!! EXPECT_CALL(foo, Bar()) .WillOnce(Return(2)) .RetiresOnSaturation(); EXPECT_CALL(foo, Bar()) .WillOnce(Return(1)) .RetiresOnSaturation();官方回应这不是 gmock 的缺陷而是你没有选对表达测试意图的方式。gmock以及 jMock的根本哲学是默认不要求期望按任何特定顺序匹配如果你需要顺序必须显式声明。这样设计的目的是让过度指定over-specify测试这件事变难防止测试因实现细节被意外写死。对于上面的需求有两种更自然的写法。写法一用 InSequence 显式排序期望按自然顺序书写// foo.Bar() should be called twice, return 1 the first time, and return // 2 the second time. Using a sequence, we can write the expectations // in their natural order. { InSequence s; EXPECT_CALL(foo, Bar()) .WillOnce(Return(1)) .RetiresOnSaturation(); EXPECT_CALL(foo, Bar()) .WillOnce(Return(2)) .RetiresOnSaturation(); }写法二把动作序列放在同一条期望里// foo.Bar() should be called twice, return 1 the first time, and return // 2 the second time. EXPECT_CALL(foo, Bar()) .WillOnce(Return(1)) .WillOnce(Return(2)) .RetiresOnSaturation();为什么 gmock 要从后往前搜索期望及 ON_CALL因为这样允许用户先为通用场景设定默认行为比如在 mock 构造器或测试夹具的 SetUp 阶段后面再用更具体的规则覆盖。如果改为从前向后搜索这个极其有用的通用默认 局部定制模式就不可能实现。十三、只设了 ON_CALL 没设 EXPECT_CALL调用时仍打印警告gmock 在整洁与安全之间坚定选择后者。ON_CALL通常写在 mock 构造器或SetUp()里作为测试间不变的行为默认值而每条测试的期望则各不相同。在 setup 阶段有 ON_CALL 不代表这些调用是被期望的——如果根本没有EXPECT_CALL而方法被调用很可能是个错误此时 gmock 若默默放行bug 就会悄悄溜进代码库。如果你确认这些调用是合法的请把ON_CALL(foo, Bar(_)) .WillByDefault(...);改成等价但声明了期望的形式EXPECT_CALL(foo, Bar(_)) .WillRepeatedly(...);这样 gmock 就知道你确实期望这些调用不再打印警告。此外也可以用--gmock_verbose控制整体输出级别调试时嫌吵就选更低级别的 verbosity。十四、如何在 action 里删除deletemock 函数的参数如果 gmock 内置 action 满足不了删除参数这类自定义副作用官方给出三条路用MakeAction()自定义单态 action用MakePolymorphicAction()自定义多态 action写一个 stub 函数用Invoke()调用它。三者各自适用场景绑定具体函数签名时Invoke()最省事要跨多种签名复用时MakePolymorphicAction()最方便需要对可用类型做精确控制时实现ActionInterface。详见 CookBook 的 Writing New Actions、Writing New Polymorphic Actions 与 Using Functions/Methods/Functors 章节。十五、MOCK_METHODn 的第二参数为什么长这样MOCK_METHODn的签名是MOCK_METHODn(Method, return_type(args))第二参数用函数类型返回类型 括号包裹的参数列表描述方法。有人建议改用MOCK_METHODn(Method, return_type, arg_1, ..., arg_n)语法官方用几个实际优势回应了这种质疑。反例mock 一个以 map 为参数的方法virtual int GetSize(const mapint, std::string m);如果采用建议语法MOCK_METHOD1(GetSize, int, const mapint, std::string m);编译器会把const mapint, std::string m解析成两个参数而不是一个直接编译失败。虽然可以用typedef起别名绕过但那是给用户添堵。gmock 的语法把参数类型保护在一对括号内天然规避了逗号歧义// This compiles fine. MOCK_METHOD1(GetSize, int(const mapint, std::string m));只有当返回类型包含未受保护的逗号时才需要typedef而这种情形要罕见得多。其他优势还包括MOCK_METHOD1(Foo, int, bool)会让读者困惑方法到底返回int还是boolgmock 语法没有这种歧义函数类型语法并非新发明——C 语言就在用TR1 的function库也大量使用而 TR1 即将并入新版 STL与之保持一致很稳妥函数类型语法还贯穿 gmock 的其他 API如 action 接口用户想用高级特性迟早要学不如从MOCK_METHOD*就统一起来。从源码看MOCK_METHOD1到MOCK_METHOD10系列宏含_T、_WITH_CALLTYPE变体均定义于 gmock-generated-function-mockers.h全部采用GMOCK_METHODn_(...)展开模式参数类型正是包裹在括号中的函数类型。十六、能 mock 静态/全局函数吗可以但需要改造代码。gmock 提醒一旦发现自己不得不 mock 静态函数通常是模块耦合过紧的信号灵活性、可复用性、可测试性都在变差。更好的做法是定义一个小的接口让被测代码通过该接口调用原函数然后 mock 这个接口。虽然初始投入多一点但通常很快就能收回成本——这正是面向接口而非面向实现编程在测试维度上的落地。十七、mock 需要做复杂的事情指定 action 太痛苦gmock 真差劲官方承认这不是一个问题但免费附赠一个答案你可能用错了工具。不用 mock 的测试通常被称为state-based testing基于状态的测试执行代码然后断言返回值正确或系统处于期望状态mock 擅长的是interaction-based testing基于交互的测试不在最后检查系统状态而是实时验证对象是否以正确的方式被调用错误一出现就立刻报告让你精确定位触发错误的上下文——这通常比状态测试更高效、更经济。如果你的测试本质是 state-based只是需要一个替身来模拟真实对象那么你需要的其实是fake假对象而不是 mock。用 mock 去做复杂动作恰恰是它的短板自然会觉得痛苦。结论不是 mock 差而是没选对工具或者你在试图解决错误的问题。十八、收到 Uninteresting function call encountered - default action taken.. 警告要慌吗完全不用这只是 FYI供你参考。它的含义是某个 mock 函数没有设置任何期望按 gmock 规则这表示你对它的调用不感兴趣允许被调用任意次数而它确实被调用了——这本身没问题你并没有声明禁止调用。但如果你本意是不允许该函数被调用只是忘了写EXPECT_CALL(foo, Bar()).Times(0)那 gmock 打印这则提示就是在帮你抓这个疏漏。所以看到这条消息时先判断这里是否本不该有调用如果是就去补上.Times(0)。为了帮你判断gmock 打印这条消息时会顺带输出函数名和参数方便定位是哪次调用。十九、自定义 action用 Invoke() 还是实现 action 接口两者皆可选更顺手的那一个action 只服务于某一种特定函数类型时用Invoke()更容易action 要复用于多种不同签名的函数例如自定义一个类似Return(value)的通用 action时MakePolymorphicAction()最简单需要精确控制action 能用于哪些函数类型时实现ActionInterface是正途。官方还给出了一个现成范本Return()的实现就在 include/gmock/gmock-actions.h 中写自定义 action 前不妨先读一读它的源码结构。二十、SetArgPointee 报 conflicting return type specified 是什么问题这个错误表示gmock不知道该给 mock 方法返回什么值。SetArgPointee()只描述了副作用把某个指针参数指向的对象改成什么并没有指定返回值。解决方法是把它和Return()用DoAll()串联起来// 同时完成“设置指针参数指向的内容”和“返回指定值”两个动作 EXPECT_CALL(foo, Bar(_)) .WillOnce(DoAll(SetArgPointee0(some_value), Return(true)));示意SetArgPointee0作用于第 0 个参数。完整示例见 CookBook 的 Mocking Side Effects 章节。二十一、问题不在这份 FAQ 里——按图索骥继续查FAQ 之外还有这些资源阅读仓库内的其他 wiki 文档ForDummies入门、CookBook配方大全、CheatSheet速查表、KnownIssues已知问题直接研读 gmock 头文件 与 测试代码——例如 gmock-matchers_test.cc、gmock-spec-builders_test.cc 展示了几乎所有内建 matcher 与期望语法的正确用法是比任何文档都权威的活教材。提问时请尽量提供以下信息信息不足时没人能帮到你所用 gmock 的版本或从 SVN 检出的修订号——gmock 活跃开发中你的问题可能已被新版修复操作系统编译器名称与版本传给编译器的完整命令行参数完整的编译器错误输出若问题涉及编译出问题的实际代码最好是能复现问题的最小完整程序。结语FAQ 背后的 gmock 设计观回看这份 FAQgmock 的每个规定背后都有一套自洽的设计哲学默认不排序期望是为了防止过度指定从后向前搜索期望是为了支持通用默认 局部覆盖保留 uninteresting-call 警告是为了在整洁与安全之间偏向安全强制MatchAndExplain统一契约是为了让 matcher 的解释信息既丰富又不拖慢不需要解释的快速路径。理解这些动机再配合本仓库 v8_6_7/testing/gmock/ 下的源码与测试用例交叉验证你就能在 miniblink49乃至任何 C 项目里把 gmock 用得既正确又优雅。【免费下载链接】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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进