19、Google Test模拟框架:GMock入门、MOCK_METHOD、EXPECT_CALL、ON_CALL

说实话,很多C++开发者对单元测试又爱又恨。爱的是它能帮你提前发现bug,恨的是——当你的函数要依赖数据库、网络、或者某个硬件设备时,测试根本没法写。这时候,模拟框架就派上用场了。

Google Test自带的GMock,就是专门干这个的。它允许你创建一个“假对象”,替代那些难以控制的真实依赖。我最早接触GMock是在一个嵌入式项目中,当时要测试一个传感器数据采集模块,但硬件还没到位。嗯,没有GMock的话,那项目进度估计要卡死。

GMock是什么?

说白了,GMock就是一个C++模拟框架。它让你能定义接口的行为,然后验证调用是否按预期发生。你想想看,测试一个支付模块时,你不可能真的去扣用户的钱吧?用GMock模拟一个支付网关,既安全又高效。

GMock的核心思想很简单:

  • 创建模拟对象:基于接口或虚类,生成一个模拟类
  • 设置期望:告诉模拟对象,某个方法应该被调用几次、传什么参数、返回什么值
  • 验证行为:测试结束后,检查所有期望是否都满足了

我在项目中遇到过好几次这样的场景:一个模块依赖了三个外部服务,每个服务都可能超时或返回错误。用GMock模拟这些服务,我可以轻松构造各种异常场景,这在真实环境中几乎不可能复现。

MOCK_METHOD:定义模拟接口

使用GMock的第一步,就是定义模拟类。假设我们有一个网络请求接口:

// 真实接口
class NetworkClient {
public:
    virtual ~NetworkClient() = default;
    virtual bool SendRequest(const std::string& url, const std::string& data) = 0;
    virtual std::string GetResponse() = 0;
};

// 模拟类
class MockNetworkClient : public NetworkClient {
public:
    MOCK_METHOD(bool, SendRequest, (const std::string& url, const std::string& data), (override));
    MOCK_METHOD(std::string, GetResponse, (), (override));
};

MOCK_METHOD的语法格式是:

MOCK_METHOD(返回值类型, 方法名, (参数列表), (限定符));

这里要注意几点:

  • 参数列表必须用括号包起来,即使没有参数也要写空括号
  • 限定符可以是 override、const、noexcept 等,多个限定符用逗号分隔
  • 模拟类必须继承自真实接口,且接口中的方法必须是虚函数
我的习惯: 我会把模拟类单独放在一个头文件中,比如 mock_network_client.h。这样测试代码和产品代码完全分离,维护起来很清爽。

EXPECT_CALL:设置调用期望

EXPECT_CALL 是GMock中最常用的宏。它用来声明你对某个方法调用的预期——包括调用次数、参数匹配、返回值等。

TEST(NetworkTest, SendRequestSuccess) {
    MockNetworkClient mock_client;
    
    // 期望 SendRequest 被调用一次,参数为特定值,返回 true
    EXPECT_CALL(mock_client, SendRequest("https://api.example.com", "hello"))
        .Times(1)
        .WillOnce(testing::Return(true));
    
    // 期望 GetResponse 被调用一次,返回模拟数据
    EXPECT_CALL(mock_client, GetResponse())
        .Times(1)
        .WillOnce(testing::Return("{\"status\":\"ok\"}"));
    
    // 执行被测代码
    DataProcessor processor(&mock_client);
    auto result = processor.FetchData();
    
    EXPECT_TRUE(result.success);
}

EXPECT_CALL 的常用匹配器:

匹配器 说明 示例
Times(n) 期望被调用 n 次 .Times(3)
Times(AtLeast(n)) 至少调用 n 次 .Times(testing::AtLeast(1))
WillOnce(action) 第一次调用时执行的动作 .WillOnce(testing::Return(42))
WillRepeatedly(action) 后续所有调用都执行该动作 .WillRepeatedly(testing::Return(0))
_ 匹配任意参数 EXPECT_CALL(mock, Foo(_))
我曾经踩过的坑: 如果 EXPECT_CALL 设置的期望没有被满足(比如调用次数不够),测试结束时 GMock 会报错。但如果你忘了写 .Times(),默认期望是“至少调用一次”。这有时候会让人困惑——明明只调了一次,为什么没报错?因为默认就是 AtLeast(1)。所以,我建议显式写出 Times 值,让意图更明确。

ON_CALL:设置默认行为

ON_CALL 和 EXPECT_CALL 很像,但目的不同。ON_CALL 只是设置一个“默认行为”,它不会验证调用次数。说白了,就是“如果这个方法被调用了,就返回这个值,但我不关心它被调了几次”。

TEST(NetworkTest, DefaultBehavior) {
    MockNetworkClient mock_client;
    
    // 设置默认行为:无论传什么参数,都返回 true
    ON_CALL(mock_client, SendRequest(testing::_, testing::_))
        .WillByDefault(testing::Return(true));
    
    // 设置默认行为:GetResponse 返回空字符串
    ON_CALL(mock_client, GetResponse())
        .WillByDefault(testing::Return(""));
    
    // 这里可以写测试逻辑,不需要担心每个方法都写 EXPECT_CALL
    DataProcessor processor(&mock_client);
    processor.Initialize();  // 内部可能调用了 SendRequest
}

ON_CALL 和 EXPECT_CALL 可以同时使用。如果同时存在,EXPECT_CALL 的优先级更高。我个人的做法是:

  • 用 ON_CALL 设置“通用默认值”,让模拟对象在大多数情况下能正常工作
  • 用 EXPECT_CALL 覆盖特定场景的期望,并验证调用次数

参数匹配与动作定制

GMock 提供了丰富的参数匹配器,让你能灵活控制哪些调用需要被匹配:

using testing::_;
using testing::Ge;
using testing::StartsWith;

EXPECT_CALL(mock, SendRequest(StartsWith("https://"), _))
    .Times(1)
    .WillOnce(testing::Return(true));

EXPECT_CALL(mock, ProcessData(Ge(100)))
    .Times(2)
    .WillRepeatedly(testing::Return("large"));

常用的动作包括:

  • Return(value):返回指定值
  • ReturnNull():返回空指针
  • Throw(exception):抛出异常
  • DoAll(a1, a2, ..., an):依次执行多个动作,最后一个动作的返回值作为最终返回值
  • Invoke(functor):调用自定义函数,实现更复杂的逻辑
实战技巧: 当需要模拟“第一次调用失败,第二次调用成功”的场景时,可以用 DoAll 配合 Return 实现:
EXPECT_CALL(mock, Connect())
    .Times(2)
    .WillOnce(testing::Return(false))   // 第一次失败
    .WillRepeatedly(testing::Return(true));  // 后续都成功

知识体系总览

下面这张图概括了GMock的核心逻辑和流程:

GMock 核心知识体系 MOCK_METHOD 定义模拟接口 ON_CALL 设置默认行为 EXPECT_CALL 设置调用期望 参数匹配器 _ , Ge() , StartsWith() 精确匹配 / 模糊匹配 动作定制 Return() / Throw() DoAll() / Invoke() 调用次数控制 Times(n) / AtLeast(n) Exactly / AnyNumber 测试结束自动验证 所有 EXPECT_CALL 期望必须满足,否则测试失败 优先级:EXPECT_CALL > ON_CALL,两者可共存

实际项目中的使用建议

我在多个项目中总结了几条经验,分享给你:

  1. 接口设计要合理:GMock 只能模拟虚函数。如果你的类没有虚函数,或者直接依赖具体类而不是接口,那测试会很难写。我建议从一开始就用接口抽象出依赖。
  2. 不要过度模拟:只模拟外部依赖(网络、数据库、文件系统)。内部逻辑应该用真实对象测试,否则测试就失去了意义。
  3. 小心 StrictMock:GMock 默认是 NiceMock——未设置期望的调用不会报错。如果你用 StrictMock,任何未预期的调用都会导致测试失败。这在某些场景下很有用,但也会让测试变得脆弱。我个人只在关键模块上使用 StrictMock。
  4. 善用 Invoke:当需要模拟复杂行为时,Invoke 可以调用自定义函数。比如模拟一个计数器,每次调用返回值递增。
一个小技巧: 如果你发现某个测试用例里 EXPECT_CALL 写了一大堆,那可能意味着被测函数做了太多事情。这时候不妨考虑拆分函数,让每个函数只做一件事。测试代码的质量,往往能反映出产品代码的设计质量。

GMock 的学习曲线不算陡,但需要多练。刚开始可能会觉得写模拟类很麻烦,但一旦习惯了这种模式,你会发现单元测试的覆盖率和可靠性都大幅提升。嗯,今天就先聊到这里,希望这些内容对你有帮助。


公众号:蓝海资料掘金营,微信deep3321