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::GMockGTest::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 集成测试流程 测试代码 EXPECT_CALL / ON_CALL Mock对象 MOCK_METHOD 宏生成 被测试对象 PaymentService 测试边界 验证结果 EXPECT_TRUE / ASSERT_EQ 调用记录 真实接口 数据库/网络/文件 Mock对象替代真实接口,测试代码只关注被测试对象的逻辑 验证结果时,同时检查业务输出和Mock调用记录

总结

GMock的核心价值就一句话:把外部依赖隔离掉,让测试只测你自己的逻辑。在CMake里集成它,用FetchContent拉下来,链接 GTest::GMock,然后写Mock类、打桩、验证——三步走。

我个人建议,每个项目从一开始就把Mock框架搭好。别等到代码写了几万行再回头补测试——那时候依赖关系已经乱成一团,Mock都难写。我见过太多项目,就是因为一开始没做Mock,后来测试覆盖率死活上不去。

好了,今天的内容就到这儿。记住:Mock不是造假,而是把测试的「噪音」过滤掉,让你能专注地验证核心逻辑。

小技巧:如果你在写Mock时发现某个接口特别难Mock——比如参数类型复杂、或者有模板方法——那往往说明这个接口设计得不够好。好的接口应该是「容易Mock的」。这反过来也能推动你的代码设计更合理。


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