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 等,多个限定符用逗号分隔
- 模拟类必须继承自真实接口,且接口中的方法必须是虚函数
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(_)) |
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):调用自定义函数,实现更复杂的逻辑
EXPECT_CALL(mock, Connect())
.Times(2)
.WillOnce(testing::Return(false)) // 第一次失败
.WillRepeatedly(testing::Return(true)); // 后续都成功
知识体系总览
下面这张图概括了GMock的核心逻辑和流程:
实际项目中的使用建议
我在多个项目中总结了几条经验,分享给你:
- 接口设计要合理:GMock 只能模拟虚函数。如果你的类没有虚函数,或者直接依赖具体类而不是接口,那测试会很难写。我建议从一开始就用接口抽象出依赖。
- 不要过度模拟:只模拟外部依赖(网络、数据库、文件系统)。内部逻辑应该用真实对象测试,否则测试就失去了意义。
- 小心 StrictMock:GMock 默认是 NiceMock——未设置期望的调用不会报错。如果你用 StrictMock,任何未预期的调用都会导致测试失败。这在某些场景下很有用,但也会让测试变得脆弱。我个人只在关键模块上使用 StrictMock。
- 善用 Invoke:当需要模拟复杂行为时,Invoke 可以调用自定义函数。比如模拟一个计数器,每次调用返回值递增。
GMock 的学习曲线不算陡,但需要多练。刚开始可能会觉得写模拟类很麻烦,但一旦习惯了这种模式,你会发现单元测试的覆盖率和可靠性都大幅提升。嗯,今天就先聊到这里,希望这些内容对你有帮助。
公众号:蓝海资料掘金营,微信deep3321