首页
/ GoogleTest gMock FAQ 全解:从“Mock 没生效”到定制 Action 的实战排错指南

GoogleTest gMock FAQ 全解:从“Mock 没生效”到定制 Action 的实战排错指南

2026-09-08 17:21:57作者:裘晴惠Vivianne

本文以仓库内 docs/gmock_faq.md(Legacy gMock FAQ)为骨架,系统梳理 gMock 使用过程中最常踩坑的十余个问题——从「为什么调用 mock 对象却执行了真实方法」到「为什么设了 ON_CALL 还打警告」,逐一给出结论、可运行的代码示例与设计背后的原因,并结合当前仓库中 googlemock/ 的源码与测试用例补充底层依据。读完你将能够独立诊断 gMock 的编译告警、运行期报错与堆检查失败,掌握 ON_CALL/EXPECT_CALLInSequenceRetiresOnSaturationDeleteArgDoAll 等关键 API 的正确用法,并理解何时该用 mock、何时该换用 fake 的测试方法论判断。

一、Mock 为什么“不生效”:可 Mock 性的边界

1. 调用 mock 对象时,执行的却是真实对象的方法

这是一个最常见的新手困惑。根本原因很简单:要被 mock 的方法必须是虚函数(virtual)。gMock 通过继承在派生类中覆写虚函数来拦截调用,如果方法不是 virtual,则对基类引用/指针的调用会走静态绑定直接落到真实实现上,mock 永远不会被触发。

唯一的例外是文档中提到的 高性能依赖注入技术(high-perf dependency injection technique),它可以在不把方法声明为 virtual 的情况下间接实现可 mock 性,完整做法见 docs/gmock_cook_book.md#MockingNonVirtualMethods。从源码结构看,MOCK_METHOD 展开后生成的 mock 方法(如 googlemock/include/gmock/gmock-function-mocker.hGMOCK_INTERNAL_MOCK_METHODN 系列宏)正是以 override 方式覆写基类虚函数,因此基类方法不具备虚函数语义时这条路就断了。

排查口诀:先检查基类接口是否 virtual;若设计上不允许虚函数,再考虑引入一层薄接口或采用“非虚公有方法 + 虚私有钩子”的高性能 DI 模式。

2. 能不能 mock 可变参数函数(variadic function)?

不能直接 mock。带省略号(...)参数的函数本质上是 C 时代的遗产,不是纯正的 C++ 特性。mock 对象在编译期无法得知调用方到底传入了多少个参数、参数分别是什么类型——这份“协议”只存在于基类作者的头脑中,gMock 不可能猜出来。

对策是由使用者主动教 mock 如何识别参数,最常见的做法是为该函数提供一组重载版本,把变参协议收敛成确定签名。同时文档强烈建议:变参函数本身不安全,且不能配合带构造/析构函数的参数对象使用,在 C++ 中应尽量避免。

3. 想 mock 静态/全局函数怎么办?

可以,但必须对代码结构做出调整。文档指出一个信号:当你发现不得不 mock 静态函数时,往往意味着模块之间耦合过紧——可复用性、可测试性都因此受损。更好的出路是抽出一个小接口,让业务代码经由该接口调用底层能力,再由 gMock 轻松 mock 这个接口。

这种“把静态依赖变成接口注入”的改造最初有一点工作量,但通常很快就能收回成本(这也是依赖注入与面向接口编程的经典收益)。文档中提到的思路可类比 Google Testing Blog 上广为流传的 “Defeat Static Cling” 一文的核心论点:静态调用是测试的“粘性依附”,需要通过解耦来摆脱。

二、编译期问题排查

4. MSVC 警告 C4301 / C4373:mock 方法带 const 参数

在 Microsoft Visual C++ 2005 SP1 上编译以下代码,可能报 C4301:

class Foo {
 public:
  virtual void Bar(const int i) = 0;
};

class MockFoo : public Foo {
 public:
  MOCK_METHOD(void, Bar, (const int i), (override));
};
warning C4301: 'MockFoo::Bar': overriding virtual function only differs from 'Foo::Bar' by const/volatile qualifier

Visual C++ 2008 SP1 则报 C4373:

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 会被忽略,所以上面 Foo::Bar(const int) 与下面的声明完全等价:

class Foo {
 public:
  virtual void Bar(int i) = 0;  // int 或 const int 没有任何区别
};

你甚至可以声明为 Bar(int)、定义时写成 Bar(const int),编译器仍会把它们视为同一个函数。

因此推荐的规避方式是:FooMockFoo 两处都把顶层 const 去掉

需要特别强调的是,这里讨论的只是顶层 const。当参数是指针或引用时,对所指对象(pointee/referee)加 const 仍然有意义,下面两个声明并不等价

void Bar(int* p);        // p 和 *p 都不是 const
void Bar(const int* p);  // p 不是 const,但 *p 是

5. SetArgPointee() 放进 WillOnce() 却报 “conflicting return type specified”

这个编译错误意味着:gMock 不知道调用该 mock 方法时应该返回什么值SetArgPointee() 只描述了“副作用”(往第 N 个指针参数指向的位置写入值),却完全没有说明返回值。

解决办法是用 DoAll() 把副作用与返回动作串起来——先 SetArgPointee() 写参数,再用 Return() 补上与真实 API 约定相符的返回值:

EXPECT_CALL(mock, GetFoo(_))
    .WillOnce(DoAll(SetArgPointee<0>(42), Return(true)));

完整示例见 docs/gmock_cook_book.md#MockingSideEffects

6. 巨大的 mock 类在 MSVC 下编译内存耗尽

经验观察表明:开启 /clr 编译选项时,Visual C++ 编译一个 mock 类所需内存会膨胀到原来的 5~6 倍。对策很直接——编译原生 C++ mock 时不要开启 /clr

三、运行期行为与预期不符的调试方法

7. gMock 认为我的预期没被满足,怎么定位?

最有效的第一步:--gmock_verbose=info 重新运行测试。该标志会让 gMock 打印出它收到的每一次 mock 函数调用的完整踪迹。仔细研读调用轨迹,往往立刻就能看出你设置的预期与实际调用错在哪里。

--gmock_verbose 的三个合法取值在 googlemock/include/gmock/internal/gmock-internal-utils.h 中有明确定义:

取值 常量 含义
info kInfoVerbosity 打印全部日志(信息 + 警告)
warning kWarningVerbosity 只打印警告(默认值
error kErrorVerbosity 不打印任何日志

默认值为 warning,定义于 googlemock/src/gmock.ccGMOCK_DEFINE_string_(verbose, testing::internal::kWarningVerbosity, ...))。

如果看到消息 “The mock function has no default action set, and its return type has no default value set.”,说明该 mock 方法既没有匹配的预期,也没有默认动作可供执行。文档建议补充一个默认动作(ON_CALL),参见 docs/gmock_cheat_sheet.md#OnCall。另需注意一个已知问题:对于没有默认动作的 mock,其“意外调用”不会打印实际参数与期望参数的详细比对,所以补齐 ON_CALL 往往也能顺带获得更详细的诊断信息。

从源码看,报告意外调用/无趣调用的统一入口是 ReportUninterestingCall()googlemock/src/gmock-spec-builders.cc):当 verbose == info 时,日志会附带 3 层栈回溯(stack_frames_to_skip = 3),否则不带栈。

8. 同一个预期失败信息被打印了两次,是不是冗余?

不是冗余。gMock 每次检测到失败,都会打印该 mock 函数的实参、相关预期的状态等信息用于排错;下一次再检测到失败时,它会把当时的预期状态再打印一遍。

两次输出内容相同,恰恰是因为两个失败时刻之间预期状态没有变化——但它们是针对不同时间点的两次快照,说明“状态在这段时间里没有推进”本身就是一条有价值的线索。

9. 崩溃后 ScopedMockLog 疯狂刷屏,是 gMock 的 bug 吗?

大概率两者都工作正常。测试崩溃时,失败信号处理器会尝试记录大量信息(栈回溯、地址映射等);如果程序多线程且栈都很深,日志量会叠加放大。ScopedMockLog 拦截到这些日志后,凡是匹配不上任何预期的都会各打一条错误。

你可以选择“学会忽略”这些噪音;更稳妥的做法是把预期写得更有针对性,例如显式忽略掉非本文件产生的日志:

using ::testing::AnyNumber;
using ::testing::Not;
...
// 忽略任何不是由本文件产生的日志。
EXPECT_CALL(log, Log(_, Not(EndsWith("/my_file.cc")), _))
    .Times(AnyNumber());

四、预期与调用次数的正确姿势

10. 如何断言某个函数“绝不能被调用”?

Times(0)

using ::testing::_;
...
EXPECT_CALL(foo, Bar(_))
    .Times(0);

11. 为什么“后写的预期会覆盖先写的”?这设计太别扭了

很多人的痛点是这样一段代码——想让 foo.Bar() 被调用两次,第一次返回 1、第二次返回 2,却不得不把预期倒着写

using ::testing::Return;
...
// 期望 foo.Bar() 被调用两次:第一次返回 1,第二次返回 2。
// 但居然要按相反的顺序写预期,体验很差!
EXPECT_CALL(foo, Bar())
    .WillOnce(Return(2))
    .RetiresOnSaturation();
EXPECT_CALL(foo, Bar())
    .WillOnce(Return(1))
    .RetiresOnSaturation();

文档指出:这不是规则的问题,而是没选对表达测试意图的最佳方式。gMock(与 jMock)的基本哲学是:默认情况下预期不要求按任何特定顺序匹配;若想约束顺序就必须显式声明。这样设计是为了避免开发者在不知不觉中过度约束测试——顺序匹配往往是测试不需要的额外限制,gMock 刻意让它“难以被顺手写出来”。

同样意图有两种更优雅的写法。

写法一:放进显式序列(InSequence),保持自然书写顺序

using ::testing::Return;
...
{
  InSequence s;
  EXPECT_CALL(foo, Bar())
      .WillOnce(Return(1))
      .RetiresOnSaturation();
  EXPECT_CALL(foo, Bar())
      .WillOnce(Return(2))
      .RetiresOnSaturation();
}

InSequence 在栈上构造一个隐式序列对象,作用于当前线程(其线程局部实现见 googlemock/src/gmock-spec-builders.ccg_gmock_implicit_sequence;类定义见 googlemock/include/gmock/gmock-spec-builders.h)。

写法二:把连续动作写进同一条预期

using ::testing::Return;
...
// foo.Bar() 应被调用两次,第一次返回 1,第二次返回 2。
EXPECT_CALL(foo, Bar())
    .WillOnce(Return(1))
    .WillOnce(Return(2))
    .RetiresOnSaturation();

.RetiresOnSaturation() 的作用是:该预期的调用次数达到上限后自动“退休”,不再与后续调用匹配,从而避免与后设的预期互相干扰。

至于“为什么要从后往前搜索预期(以及 ON_CALL)”,源码给出了清晰答案:googlemock/include/gmock/gmock-spec-builders.hFindMatchingExpectationLocked() 使用 const_reverse_iteratoruntyped_expectations_ 从最新到最旧查找第一个能处理当前实参的预期;ON_CALL 默认动作的查找同样从后向前(同文件 L1480 附近)。这样设计的好处是:你可以在测试早期(比如 mock 构造函数或 fixture 的 set-up 阶段)先建立“通用默认行为”,再用后面更具体的规则覆盖它。如果改成从前往后搜索,这套“默认 + 特化”的惯用模式将无法成立。

12. 只用 ON_CALL 设了行为,方法被调用时 gMock 仍打印警告?

这是故意为之——在“整洁”与“安全”之间,gMock 选择安全。开发者常在 mock 构造函数或 SetUp() 里写 ON_CALL(因为默认行为跨测试很少变化),然后在测试体中写各不相同的 EXPECT_CALL。但“设置了 ON_CALL”绝不等于“这些调用是被期望的”。如果根本没有 EXPECT_CALL 而方法却被调用了,那很可能就是一处错误——若 gMock 静默放行,bug 就会在无人察觉的情况下滋生。

如果你确信这些调用没有问题,把 ON_CALL 换成带 .WillRepeatedly(...)EXPECT_CALL 即可消除告警:

using ::testing::_;
...
EXPECT_CALL(foo, Bar(_))
    .WillRepeatedly(...);   // 显式声明“你确实期望这些调用”

而不是:

using ::testing::_;
...
ON_CALL(foo, Bar(_))
    .WillByDefault(...);    // 只是设定默认行为

此外可用 --gmock_verbose=error 降低输出噪音(其余取值 infowarning 见上文表格)。调试时若嫌输出太吵,调低 verbose 级别即可。

13. “Uninteresting function call encountered - default action taken..” 需要恐慌吗?

完全不用,这只是一条提示(FYI)。它的含义是:有一个 mock 方法,你没对它设任何预期(按 gMock 的规则,这意味着“你不关心对该函数的调用”,因此它可以被调用任意多次),而现在它确实被调用了——这并不违反任何约定。

当然,如果你本意是禁止该函数被调用、只是忘了写 EXPECT_CALL(foo, Bar()).Times(0),那确实属于失误。虽然可以辩解这是使用者的责任,但 gMock 仍然贴心地把提示打出来提醒你。

所以当你看到这条信息且确信不该有无趣调用发生时,就应该去调查到底发生了什么。gMock 会在遇到无趣调用时打印栈回溯,借助它可以快速定位是哪个 mock 函数、在什么调用路径上被触发。

补充:gMock 对“无趣调用”的处理策略与 mock 的类型有关。从 googlemock/include/gmock/gmock-nice-strict.h 提供的 NiceMock<T> / NaggyMock<T> / StrictMock<T> 与上文 ReportUninterestingCall()kAllow / kWarn / 失败三种分支可以对应起来:Nice mock 静默放行、普通 mock 打警告(默认)、StrictMock 则直接判失败。

14. 什么是“状态测试”与“交互测试”?为什么我用 mock 做复杂的事很痛苦?

先给结论:当你在做基于状态的测试、仅仅需要一个替身来模拟真实对象时,用 fake(假实现)而不是 mock 会更合适。mock 的长处是“交互式测试”,不是“执行复杂动作”。

  • 状态测试(state-based testing):不借助 mock 写测试时,你执行代码并断言返回值正确、或系统处于预期状态。
  • 交互测试(interaction-based testing):mock 的强项——不是到最后才检查系统状态,而是在被正确的方式调用的瞬间逐次验证,一旦出错立刻上报,让你能精确掌握错误被触发的上下文。对许多场景而言,这比状态测试更有效、更经济。

如果你用 mock 去模拟一个需要复杂内部逻辑的真实对象,自然会觉得“gMock 真难用”——因为你用错了工具。mock 不擅长承载复杂动作,这类工作应当交给以真实逻辑实现的 fake。

五、自定义 Action 与参数处理

15. 如何在 action 里删除 mock 函数的某个参数?

如果 mock 函数接收指针参数,而你希望在调用时删除它,可使用 testing::DeleteArg<N>() 删除**第 N 个(从 0 计数)**参数:

using ::testing::_;
...
MOCK_METHOD(void, Bar, (X* x, const Y& y));
...
EXPECT_CALL(mock_foo_, Bar(_, _))
    .WillOnce(testing::DeleteArg<0>());

该 action 的声明位于 googlemock/include/gmock/gmock-actions.hinternal::DeleteArgAction<k>),对应测试见 googlemock/test/gmock-more-actions_test.cc。注意它只能用于指针参数——同文件 L1796 的断言信息明确提示了“对非指针参数使用 DeleteArg”属于误用。

16. 如何对 mock 函数的参数执行任意自定义动作?

当 gMock 内置的 action 不够用时,有三条路:

  1. MakeAction() 定义单态自定义动作;
  2. MakePolymorphicAction() 定义多态自定义动作;
  3. 写一个桩函数(stub),再用 Invoke() 调用它。

示例:

using ::testing::_;
using ::testing::Invoke;
...
MOCK_METHOD(void, Bar, (X* p));
...
EXPECT_CALL(mock_foo_, Bar(_))
    .WillOnce(Invoke(MyAction(...)));

17. 自定义 action:用 Invoke() 还是实现 ActionInterface 接口?

都可以,按场景选更顺手的那一种:

  • 如果这个 action 只服务于某一特定函数签名,用 Invoke() 通常更简单;
  • 如果这个 action 要用于多种不同签名的函数(比如你自己实现一个 Return(*value*)),MakePolymorphicAction() 最省事;
  • 如果你希望对 action 可用的函数类型做精确控制,就去实现 ActionInterface<T> 接口——gMock 内置的 Return() 就是这么实现的,可在 googlemock/include/gmock/gmock-actions.h 中查看它的实现作为范本。

更系统的讲解可分别参考 docs/gmock_cook_book.md#NewMonoActions(新单态动作)、docs/gmock_cook_book.md#NewPolyActions(新多态动作)与 docs/gmock_cook_book.md#FunctionsAsActions(函数即动作)。

六、资源泄漏与内存问题

18. 用 mock 对象时 heapcheck 失败,用真实对象却没事?

先问自己一个问题:被 mock 的类(最好是一个纯接口)有没有虚析构函数?

只要从基类做派生,基类析构函数就必须是 virtual,否则后果很严重。看这个例子:

class Base {
 public:
  // 非虚析构,但这里本该是 virtual。
  ~Base() { ... }
  ...
};

class Derived : public Base {
 public:
  ...
 private:
  std::string value_;
};

...
Base* p = new Derived;
...
delete p;  // 意外!只会调用 ~Base(),不会调用 ~Derived()
           // —— value_ 就这样泄漏了。

~Base() 改成 virtual 后,执行 delete p 时会正确地依次调用 ~Derived()~Base(),堆检查器自然就满意了。

结语:把 FAQ 变成你的调试清单

这份 FAQ 表面上回答的是一个个零散问题,但背后贯穿着 gMock 的几条核心设计原则,理解了它们,绝大多数问题都能自行推导:

  1. 能 mock 的只有虚函数——接口设计决定可测试性;
  2. “默认放行、按需约束”——预期默认无序、无趣调用默认允许,InSequence/Times(0)/StrictMock 才是显式收紧的手段;
  3. 默认行为与期望行为分层——ON_CALL 设默认值,EXPECT_CALL 设验证,两者职责不同,自然解释了许多“为什么还报警告”的现象;
  4. 从后向前匹配——新预期覆盖旧预期,使“先通用后特化”的测试编排成为可能。

实际调试中,--gmock_verbose=info 的完整调用轨迹 + 无趣调用附带的栈回溯 + 补全默认动作后的详细参数比对,基本能覆盖九成以上的“预期不满足”问题。本文所涉主题的展开教程(含大量可运行样例)可在 docs/gmock_for_dummies.mddocs/gmock_cook_book.mddocs/gmock_cheat_sheet.md 中继续深入;对应实现细节以 googlemock/include/gmock/googlemock/src/googlemock/test/ 下的源码和测试为准。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395