第七章:模拟对象(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):等于 xtesting::Ne(x):不等于 xtesting::Lt(x)/Gt(x):小于 / 大于 xtesting::HasSubstr(s):包含子串 stesting::StartsWith(s)/EndsWith(s):以 s 开头 / 结尾testing::AllOf(a, b):同时满足 a 和 btesting::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 相关的坑我踩了不少。分享几个典型的:
- 忘记写
override:MOCK_METHOD宏生成的函数必须和基类签名完全一致。漏了override会导致编译通过但运行时多态失效。我曾经排查了一下午,最后发现是少写了const。 - 过度 Mock:能不用 Mock 就别用。如果一个类只是简单的数据容器,直接传真实对象就好。Mock 会增加维护成本。
- 忽略默认行为:Google Mock 对未设置期望的方法,默认返回默认值(比如 int 返回 0,指针返回 nullptr)。这可能导致测试通过但实际行为不对。我建议对每个 Mock 方法都显式设置期望,哪怕只是
EXPECT_CALL(...).Times(AnyNumber())。 - 顺序约束滥用:
InSequence很强大,但别到处用。只有顺序真正影响业务逻辑时才加。否则测试会变得脆弱——你改一下无关代码的顺序,测试就红了。
7.7 知识体系总览
下面这张图总结了 Google Mock 的核心概念和它们之间的关系:
7.8 写在最后
Mock 是 TDD 工具箱里的一把瑞士军刀。它让你在隔离环境中验证代码行为,而不必依赖真实的数据库、网络或文件系统。
我个人习惯在项目初期就引入 Mock。不是因为我有洁癖,而是因为——等到代码写完了再补 Mock,你会发现很多接口设计得根本没法 mock。到时候改代码,成本就高了。
记住:好的 Mock 测试,应该是「写起来顺手,读起来明白,改起来不痛」。
一句话总结:Google Mock 帮你验证三件事——调没调、传没传对、顺序对不对。用好它,你的单元测试会从「能跑」变成「靠谱」。