第七章:模拟对象(Mock)——用 Google Mock 打造你的测试替身

说实话,很多 C++ 开发者第一次接触单元测试时,都会遇到同一个问题:我的函数依赖数据库、网络或者硬件,怎么测?

我记得刚带团队那会儿,有个同事写了 2000 行业务代码,一跑单元测试就报错——因为代码里直接调了第三方支付接口。测试环境根本没那个服务。他愁得不行,问我:「这代码是不是就没法测了?」

我说:「别急,咱们给它造个假接口。」

这就是 Mock 的用武之地。

7.1 什么是 Mock?为什么需要它?

Mock 对象,说白了就是一个「替身」。它长得像真实对象,行为却由你控制。你可以让它返回固定值、抛出异常、甚至记录自己被调用了多少次。

我个人习惯把 Mock 分成两类:

  • 桩对象(Stub):只负责返回预设数据。比如「调用 GetUser() 永远返回一个默认用户」。
  • 模拟对象(Mock):不仅返回数据,还能验证调用次数、参数、顺序。这才是真正的「测试间谍」。

Google Mock 是 Google Test 家族的一员。它把这两件事合在一起做了。你想想看,一个工具既能打桩又能验证行为,多省事。

核心要点:Mock 不是为了「绕过测试」,而是为了让你隔离依赖,专注测试当前模块的逻辑。

7.2 快速上手:第一个 Google Mock 示例

假设我们有一个网络请求类 HttpClient

class HttpClient {
public:
    virtual ~HttpClient() = default;
    virtual bool SendRequest(const std::string& url, const std::string& data) = 0;
    virtual std::string GetResponse() = 0;
};

现在我们要测试一个 OrderService,它依赖 HttpClient 来下单:

class OrderService {
public:
    explicit OrderService(HttpClient* client) : client_(client) {}
    
    bool PlaceOrder(const std::string& item, int quantity) {
        std::string data = "item=" + item + "&qty=" + std::to_string(quantity);
        if (!client_->SendRequest("/api/order", data)) {
            return false;
        }
        std::string resp = client_->GetResponse();
        return resp.find("success") != std::string::npos;
    }
    
private:
    HttpClient* client_;
};

用 Google Mock 来模拟 HttpClient

#include <gmock/gmock.h>
#include <gtest/gtest.h>

class MockHttpClient : public HttpClient {
public:
    MOCK_METHOD(bool, SendRequest, (const std::string& url, const std::string& data), (override));
    MOCK_METHOD(std::string, GetResponse, (), (override));
};

TEST(OrderServiceTest, PlaceOrderSuccess) {
    MockHttpClient mock_client;
    OrderService service(&mock_client);
    
    // 设置桩行为
    EXPECT_CALL(mock_client, SendRequest(testing::_, testing::_))
        .WillOnce(testing::Return(true));
    EXPECT_CALL(mock_client, GetResponse())
        .WillOnce(testing::Return("{\"status\":\"success\"}"));
    
    bool result = service.PlaceOrder("book", 2);
    EXPECT_TRUE(result);
}

你看,MOCK_METHOD 宏帮我们生成了模拟方法。EXPECT_CALL 既设定了返回值,又隐含了「这个方法必须被调用一次」的验证。

小技巧testing::_ 是通配符,表示「匹配任何参数」。如果你要精确匹配,直接写具体值就行。

7.3 验证调用次数:不止是「调没调」

我在项目中遇到过一个问题:某个缓存模块在极端情况下被重复调用了三次,导致性能雪崩。用 Mock 的次数验证,一下子就抓到了。

Google Mock 提供了丰富的次数匹配器:

匹配器 含义 示例
Times(n) 恰好调用 n 次 Times(3)
Times(AtLeast(n)) 至少 n 次 Times(AtLeast(1))
Times(AtMost(n)) 至多 n 次 Times(AtMost(2))
Times(Between(a, b)) 调用次数在 a 到 b 之间 Times(Between(1, 5))
WillOnce() + WillRepeatedly() 前 n 次返回 A,之后返回 B 见下方示例
// 验证:SendRequest 必须被调用恰好 2 次
EXPECT_CALL(mock_client, SendRequest(testing::_, testing::_))
    .Times(2)
    .WillRepeatedly(testing::Return(true));

// 验证:第一次返回 true,第二次返回 false
EXPECT_CALL(mock_client, SendRequest(testing::_, testing::_))
    .WillOnce(testing::Return(true))
    .WillOnce(testing::Return(false));

注意:如果你不写 Times(),默认是 Times(1)。也就是说,EXPECT_CALL 默认期望该方法恰好被调用一次。如果没调或者调了多次,测试会失败。

7.4 验证参数:你的函数传对了没?

有时候,调用次数对了还不够。参数传错了,照样出 bug。

我曾经接手过一个支付模块,同事把金额单位「分」传成了「元」,结果一笔 100 元的订单变成了 10000 元。这种 bug 如果能在单元测试里用参数匹配器抓住,就不会流到线上。

Google Mock 的参数匹配器非常强大:

// 精确匹配:url 必须是 "/api/order",data 必须包含 "item=book"
EXPECT_CALL(mock_client, SendRequest("/api/order", testing::HasSubstr("item=book")))
    .WillOnce(testing::Return(true));

// 使用自定义匹配器:验证 data 符合特定格式
MATCHER_P(DataContains, keyword, "data contains " + keyword) {
    return arg.find(keyword) != std::string::npos;
}
EXPECT_CALL(mock_client, SendRequest(testing::_, DataContains("qty=2")))
    .WillOnce(testing::Return(true));

常用的参数匹配器:

  • testing::Eq(x):等于 x
  • testing::Ne(x):不等于 x
  • testing::Lt(x) / Gt(x):小于 / 大于 x
  • testing::HasSubstr(s):包含子串 s
  • testing::StartsWith(s) / EndsWith(s):以 s 开头 / 结尾
  • testing::AllOf(a, b):同时满足 a 和 b
  • testing::AnyOf(a, b):满足 a 或 b

7.5 验证调用顺序:有时候顺序就是一切

嗯,这里要注意。有些场景下,调用顺序比次数更重要。比如:必须先打开文件,再写入数据,最后关闭。顺序乱了,程序就崩了。

Google Mock 提供了 InSequence 来约束顺序:

using testing::InSequence;

TEST(OrderServiceTest, VerifyCallOrder) {
    InSequence seq;  // 从此往后,EXPECT_CALL 的顺序就是调用顺序
    
    MockHttpClient mock_client;
    OrderService service(&mock_client);
    
    // 必须按这个顺序调用
    EXPECT_CALL(mock_client, SendRequest(testing::_, testing::_))
        .WillOnce(testing::Return(true));
    EXPECT_CALL(mock_client, GetResponse())
        .WillOnce(testing::Return("{\"status\":\"success\"}"));
    
    service.PlaceOrder("book", 2);
}

如果代码先调了 GetResponse() 再调 SendRequest(),测试就会失败。这个特性在测试状态机、协议栈时特别有用。

7.6 避坑指南:我踩过的那些 Mock 坑

做 TDD 这么多年,Mock 相关的坑我踩了不少。分享几个典型的:

  1. 忘记写 overrideMOCK_METHOD 宏生成的函数必须和基类签名完全一致。漏了 override 会导致编译通过但运行时多态失效。我曾经排查了一下午,最后发现是少写了 const
  2. 过度 Mock:能不用 Mock 就别用。如果一个类只是简单的数据容器,直接传真实对象就好。Mock 会增加维护成本。
  3. 忽略默认行为:Google Mock 对未设置期望的方法,默认返回默认值(比如 int 返回 0,指针返回 nullptr)。这可能导致测试通过但实际行为不对。我建议对每个 Mock 方法都显式设置期望,哪怕只是 EXPECT_CALL(...).Times(AnyNumber())
  4. 顺序约束滥用InSequence 很强大,但别到处用。只有顺序真正影响业务逻辑时才加。否则测试会变得脆弱——你改一下无关代码的顺序,测试就红了。

7.7 知识体系总览

下面这张图总结了 Google Mock 的核心概念和它们之间的关系:

Google Mock 验证调用次数 验证调用参数 验证调用顺序 Times(n) AtLeast / AtMost WillOnce / WillRepeatedly Eq / Ne / Lt / Gt HasSubstr / StartsWith AllOf / AnyOf InSequence

7.8 写在最后

Mock 是 TDD 工具箱里的一把瑞士军刀。它让你在隔离环境中验证代码行为,而不必依赖真实的数据库、网络或文件系统。

我个人习惯在项目初期就引入 Mock。不是因为我有洁癖,而是因为——等到代码写完了再补 Mock,你会发现很多接口设计得根本没法 mock。到时候改代码,成本就高了。

记住:好的 Mock 测试,应该是「写起来顺手,读起来明白,改起来不痛」。

一句话总结:Google Mock 帮你验证三件事——调没调、传没传对、顺序对不对。用好它,你的单元测试会从「能跑」变成「靠谱」。


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