61、Google Mock:集成GMock,进行接口模拟测试
说实话,单元测试里最让人头疼的就是「依赖」——你的函数要调数据库、要发HTTP请求、要读写文件……这些外部依赖一掺和进来,测试就变得又慢又不稳定。我当年在一个支付项目里就吃过这个亏:每次跑测试都得连测试库,结果同事一跑批量数据,我的测试就全挂了。
后来我学乖了——用Mock。说白了,就是造一个「假对象」来替代真实依赖。Google Mock(简称GMock)就是干这个的利器。今天咱们就聊聊,怎么在CMake项目里集成GMock,把接口模拟玩明白。
GMock是什么?
GMock是Google Test家族的一员。它专门用来创建模拟对象(Mock Object)。你想想看,当你测试一个支付服务时,并不想真的去调银行接口——太慢、太贵、还可能因为网络波动导致测试失败。这时候,一个Mock对象就能完美替代真实接口。
GMock的核心能力就两个:
- 打桩(Stub):指定某个方法被调用时返回什么值
- 验证(Verify):检查某个方法是否被调用、调了几次、传了什么参数
我个人习惯把Mock用在「边界」上——凡是跨进程、跨网络、跨设备的调用,一律Mock掉。这样测试跑起来飞快,而且结果可重复。
在CMake中集成GMock
集成方式其实很简单。我推荐用FetchContent,这样不用手动下载源码,CMake自己搞定。
# CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(GMockDemo CXX)
set(CMAKE_CXX_STANDARD 17)
# 引入Google Test(包含GMock)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip
)
FetchContent_MakeAvailable(googletest)
# 启用GMock
enable_testing()
# 你的业务代码(被测试的)
add_library(PaymentService STATIC
src/PaymentService.cpp
)
# 测试目标
add_executable(test_payment
tests/test_payment.cpp
)
target_link_libraries(test_payment
PRIVATE
PaymentService
GTest::GTest
GTest::Main
GTest::GMock
GTest::GMockMain
)
add_test(NAME PaymentTest COMMAND test_payment)
嗯,这里要注意:GTest::GMock 和 GTest::GMockMain 都要链接上。前者是Mock库,后者提供了main函数入口。如果你自己写了main,可以只链接前者。
实战:模拟一个支付网关接口
假设我们有一个支付服务,它依赖一个外部网关接口:
// include/IPaymentGateway.h
class IPaymentGateway {
public:
virtual ~IPaymentGateway() = default;
virtual bool charge(const std::string& userId, double amount) = 0;
virtual double getBalance(const std::string& userId) = 0;
};
现在我们要测试 PaymentService 里的逻辑,但不想真的去调网关。那就造一个Mock:
// tests/mock_payment_gateway.h
#include <gmock/gmock.h>
#include "IPaymentGateway.h"
class MockPaymentGateway : public IPaymentGateway {
public:
MOCK_METHOD(bool, charge, (const std::string&, double), (override));
MOCK_METHOD(double, getBalance, (const std::string&), (override));
};
看到 MOCK_METHOD 宏了吗?这是GMock的核心。它自动帮你生成一个模拟版本的方法。你只需要告诉它:方法名、参数类型、返回值类型。
然后写测试:
// tests/test_payment.cpp
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "mock_payment_gateway.h"
#include "PaymentService.h"
using ::testing::Return;
using ::testing::_;
using ::testing::Exactly;
TEST(PaymentServiceTest, ChargeSuccess) {
// 创建Mock对象
MockPaymentGateway mockGateway;
// 打桩:当charge被调用时,返回true
EXPECT_CALL(mockGateway, charge("user_123", 99.0))
.Times(Exactly(1))
.WillOnce(Return(true));
// 创建被测试对象,注入Mock
PaymentService service(&mockGateway);
bool result = service.processPayment("user_123", 99.0);
EXPECT_TRUE(result);
}
TEST(PaymentServiceTest, ChargeFailure) {
MockPaymentGateway mockGateway;
// 这次模拟失败
EXPECT_CALL(mockGateway, charge(_, _))
.Times(Exactly(1))
.WillOnce(Return(false));
PaymentService service(&mockGateway);
bool result = service.processPayment("user_123", 99.0);
EXPECT_FALSE(result);
}
这里有几个关键点:
_是通配符,表示「任何值都行」Times(Exactly(1))表示这个方法必须被调用恰好一次WillOnce(Return(true))表示第一次调用时返回true
我曾经踩过一个坑:忘记写 Times(),结果方法被调了两次但只期望一次,测试居然通过了——因为GMock默认的期望是「至少一次」。后来我养成了习惯,每个 EXPECT_CALL 都显式指定调用次数。
GMock的匹配器(Matcher)
匹配器是GMock的精华。它让你能灵活地匹配参数。我常用的几个:
| 匹配器 | 含义 | 示例 |
|---|---|---|
_ |
匹配任意值 | charge(_, 99.0) |
Eq(x) |
等于x | charge(Eq("user_123"), _) |
Ne(x) |
不等于x | charge(Ne("admin"), _) |
Gt(x) |
大于x | charge(_, Gt(0.0)) |
Lt(x) |
小于x | charge(_, Lt(10000.0)) |
StartsWith(s) |
字符串以s开头 | charge(StartsWith("user_"), _) |
匹配器可以组合使用。比如你想匹配「金额在10到100之间」:
using ::testing::AllOf;
using ::testing::Gt;
using ::testing::Lt;
EXPECT_CALL(mockGateway, charge(_, AllOf(Gt(10.0), Lt(100.0))))
.Times(Exactly(1))
.WillOnce(Return(true));
说实话,匹配器这东西,刚开始用觉得麻烦,用久了就离不开。它让测试代码变得极其灵活——你不需要精确匹配每个参数,只需要描述「什么样的参数是合法的」。
动作(Action)的高级用法
除了简单的 Return,GMock还支持更复杂的动作:
using ::testing::Return;
using ::testing::DoAll;
using ::testing::SaveArg;
using ::testing::SetArgReferee;
// 保存参数
std::string savedUserId;
EXPECT_CALL(mockGateway, charge(_, _))
.WillOnce(DoAll(
SaveArg<0>(&savedUserId),
Return(true)
));
// 修改引用参数
EXPECT_CALL(mockGateway, getBalance(_))
.WillOnce(DoAll(
SetArgReferee<0>(100.0),
Return(true)
));
我在一个日志系统里用过 SaveArg——我需要验证日志内容是否正确,但又不想真的写文件。用SaveArg把日志内容存下来,然后断言内容是否符合预期。这招特别好使。
避坑指南
我曾经踩过的坑:
- 忘记调用基类析构函数:Mock类继承自接口,如果接口的析构函数不是虚函数,Mock对象析构时不会调用你的验证逻辑。记得给接口加上
virtual ~IPaymentGateway() = default; - EXPECT_CALL 和 ON_CALL 混用:
EXPECT_CALL会验证调用次数,ON_CALL只是设置默认行为。如果你只想打桩不验证,用ON_CALL;如果你想验证调用行为,用EXPECT_CALL。别混着用,容易出bug。 - Mock对象生命周期:Mock对象必须在被测试对象之前销毁。否则被测试对象析构时可能还会调用Mock方法,而Mock已经没了——直接崩溃。
GMock集成流程图
下面这张图展示了GMock在测试中的角色:
总结
GMock的核心价值就一句话:把外部依赖隔离掉,让测试只测你自己的逻辑。在CMake里集成它,用FetchContent拉下来,链接 GTest::GMock,然后写Mock类、打桩、验证——三步走。
我个人建议,每个项目从一开始就把Mock框架搭好。别等到代码写了几万行再回头补测试——那时候依赖关系已经乱成一团,Mock都难写。我见过太多项目,就是因为一开始没做Mock,后来测试覆盖率死活上不去。
好了,今天的内容就到这儿。记住:Mock不是造假,而是把测试的「噪音」过滤掉,让你能专注地验证核心逻辑。
小技巧:如果你在写Mock时发现某个接口特别难Mock——比如参数类型复杂、或者有模板方法——那往往说明这个接口设计得不够好。好的接口应该是「容易Mock的」。这反过来也能推动你的代码设计更合理。
公众号:蓝海资料掘金营,微信deep3321