C++单元测试进阶:GoogleMock集成与模拟对象实战指南

1. 项目概述:为什么我们需要GoogleMock?

在C++单元测试的世界里,GoogleTest(简称gtest)是当之无愧的基石。它能帮我们验证一个函数、一个类的方法是否按预期工作。但现实中的代码往往不是孤岛,一个类A可能依赖另一个复杂的类B,或者需要调用一个访问数据库、发送网络请求的接口。当你为A编写单元测试时,如果B还没实现,或者调用外部服务会带来不确定性(比如网络延迟、数据库状态变化),测试就会变得异常困难,甚至无法进行。这就是模拟(Mocking)框架要解决的核心问题:隔离被测对象,用可控的“替身”代替其依赖项

GoogleMock(简称gmock)正是GoogleTest生态中专司此职的强力工具。它不是一个独立的测试框架,而是与GoogleTest深度集成的模拟库。你可以把它理解为gtest的“最佳拍档”。通过gmock,你可以创建一个模拟对象,这个对象拥有和真实依赖对象相同的接口(方法),但你可以精确地预设这些方法被调用时的行为:返回什么值、抛出什么异常、甚至验证它是否被调用、以什么参数被调用、被调用了多少次。这样一来,你的测试就从一个充满不确定性的“集成测试”,变成了一个在纯净、可控环境下运行的“单元测试”。测试的焦点重新回到了被测对象自身的逻辑上,测试用例的执行速度更快,结果也更稳定可靠。

我见过很多团队在引入单元测试时,初期只使用gtest,测试一些工具函数尚可,一旦遇到有依赖的类就束手无策,要么跳过不测,要么写一些脆弱且缓慢的集成测试。这其实浪费了单元测试快速反馈的核心价值。而集成gmock,就像是给你的测试工具箱里添上了一把瑞士军刀,它能帮你拆解复杂依赖,让单元测试真正落地到业务逻辑复杂的核心模块上。接下来,我会带你从零开始,完成gmock与gtest的集成,并深入讲解如何在实际项目中用好它。

2. 环境准备与项目集成

2.1 获取与编译GoogleTest/GoogleMock

首先明确一点,从GoogleTest v1.10.0版本开始,GoogleMock已经作为其一部分被包含在内。你不再需要单独下载gmock。所以,我们的第一步是获取一个较新版本的GoogleTest源码。

最推荐的方式是通过源码构建。你可以从GitHub仓库(github.com/google/googletest)克隆或下载发布包。假设我们创建一个项目目录my_project_with_gmock,结构如下:

my_project_with_gmock/ ├── CMakeLists.txt ├── googletest/ # 这里放置googletest源码 ├── include/ # 你的项目头文件 ├── src/ # 你的项目源文件 └── tests/ # 测试代码目录

在你的项目根目录的CMakeLists.txt中,通过add_subdirectory引入googletest,并将其目标链接到你的测试可执行文件中。这是一种经典且可控的方式。

cmake_minimum_required(VERSION 3.14) project(MyProjectWithGMock) set(CMAKE_CXX_STANDARD 17) # 添加googletest子目录 add_subdirectory(googletest) # 你的主项目目标 add_executable(my_app src/main.cpp src/my_class.cpp) target_include_directories(my_app PRIVATE include) # 你的测试目标 add_executable(run_tests tests/test_my_class.cpp src/my_class.cpp) target_include_directories(run_tests PRIVATE include ${CMAKE_CURRENT_SOURCE_DIR}/googletest/googletest/include) target_link_libraries(run_tests PRIVATE gtest gmock gtest_main) # 注意:gtest_main提供了main函数,如果你的测试文件自己定义了main,则不要链接它。

这里的关键是target_link_libraries(run_tests ... gmock)。链接gmock库是启用模拟功能的核心。gtest提供了基础的断言和测试框架,gmock提供了模拟对象功能,而gtest_main则提供了一个默认的main()函数来启动测试。如果你有特殊的初始化需求(例如设置全局测试环境),可以自己编写main函数,并链接gtestgmock,但不链接gtest_main

注意:确保你的编译器支持C++11或更高标准,因为gmock大量使用了现代C++特性(如变参模板、右值引用等)。在CMake中通过set(CMAKE_CXX_STANDARD 11/14/17)来指定。

2.2 理解模拟对象的核心:接口与依赖注入

在编写模拟之前,必须理解一个关键的设计原则:依赖注入(Dependency Injection, DI)。简单说,就是不要在被测类内部硬编码(new)它的依赖对象,而是通过构造函数、Setter方法或接口参数从外部“注入”这个依赖。

为什么?因为只有依赖是从外部注入的,我们在测试时才能轻松地将真实的依赖替换成我们控制的模拟对象。

假设我们有一个OrderProcessor类,它依赖一个PaymentGateway来处理支付。糟糕的设计可能是这样的:

// 糟糕的设计:紧耦合 class OrderProcessor { public: OrderProcessor() : paymentGateway_(new RealPaymentGateway()) {} // 内部创建依赖 bool processOrder(const Order& order) { return paymentGateway_->charge(order.totalAmount); } private: PaymentGateway* paymentGateway_; };

在这个设计下,你无法在测试中替换RealPaymentGateway。好的设计应该是:

// 好的设计:依赖注入 class OrderProcessor { public: // 通过构造函数注入依赖 explicit OrderProcessor(PaymentGateway* gateway) : paymentGateway_(gateway) {} bool processOrder(const Order& order) { // 假设支付网关不可用则重试一次 if (!paymentGateway_->charge(order.totalAmount)) { // 重试逻辑 return paymentGateway_->charge(order.totalAmount); } return true; } private: PaymentGateway* paymentGateway_; // 通常使用智能指针或引用更好 };

现在,PaymentGateway成为一个抽象(基类)。在真实环境中,我们注入一个RealPaymentGateway;在测试环境中,我们注入一个MockPaymentGateway。这就是gmock发挥作用的前提。接下来,我们就来创建这个模拟类。

3. 创建与使用模拟类

3.1 定义模拟类:MOCK_METHOD宏

首先,你需要有一个待模拟的抽象基类(接口)。对于上面的例子,我们定义PaymentGateway

// include/payment_gateway.h #ifndef PAYMENT_GATEWAY_H #define PAYMENT_GATEWAY_H class PaymentGateway { public: virtual ~PaymentGateway() = default; // 虚析构函数很重要! virtual bool charge(double amount) = 0; virtual std::string getTransactionId() const = 0; virtual void logTransaction(const std::string& id, double amount) = 0; }; #endif // PAYMENT_GATEWAY_H

接着,在测试代码中(例如tests/mock_payment_gateway.h),我们使用gmock来创建模拟类:

// tests/mock_payment_gateway.h #ifndef MOCK_PAYMENT_GATEWAY_H #define MOCK_PAYMENT_GATEWAY_H #include "payment_gateway.h" #include <gmock/gmock.h> // 必须包含gmock头文件 class MockPaymentGateway : public PaymentGateway { public: // 使用 MOCK_METHOD 宏来模拟虚函数 // 格式:MOCK_METHOD(返回值类型, 方法名, (参数列表), (限定符...)); MOCK_METHOD(bool, charge, (double amount), (override)); MOCK_METHOD(std::string, getTransactionId, (), (const, override)); MOCK_METHOD(void, logTransaction, (const std::string& id, double amount), (override)); }; #endif // MOCK_PAYMENT_GATEWAY_H

关键点解析:

  1. 继承自真实接口MockPaymentGateway公开继承自PaymentGateway。这保证了模拟对象可以替换任何需要PaymentGateway的地方。
  2. MOCK_METHOD:这是gmock的核心。其参数依次为:返回值类型、方法名、参数列表(用括号括起来)、以及一个可选的限定符列表(也用括号括起来)。
  3. 限定符(override)是C++11的关键字,表明这是重写基类的虚函数,这里主要是为了代码清晰和编译器检查。(const)表明这个方法是const的,必须与基类声明一致。如果方法有noexceptvirtual等修饰符,也需要在限定符列表中注明,例如(override, noexcept)
  4. 包含头文件:务必#include <gmock/gmock.h>。通常,在测试文件中,你也会#include <gtest/gtest.h>,但gmock头文件已经包含了gtest的必要部分。

实操心得:对于有多个重载版本的方法,你需要为每个重载版本单独使用MOCK_METHOD宏。gmock会根据参数列表来区分它们。

3.2 设置预期行为:ON_CALL 与 EXPECT_CALL

创建了模拟对象后,下一步是告诉它:“当某个方法被调用时,你应该如何反应”。这称为“设置预期”(Setting Expectations)。gmock提供了两个主要的宏:ON_CALLEXPECT_CALL。它们看似相似,但意图有微妙而重要的区别。

  • ON_CALL:定义默认行为。它告诉模拟对象:“当这个方法被调用时,如果没有更具体的EXPECT_CALL约束,你就按这个方式来反应”。它更侧重于提供行为(Action),而不强调必须被调用。

    using ::testing::Return; MockPaymentGateway mockGateway; // 设置默认行为:当charge被调用时,默认返回true ON_CALL(mockGateway, charge).WillByDefault(Return(true));
  • EXPECT_CALL:设置期望并定义行为。它同时做了两件事:1) 声明我期望这个方法会被调用(可能带有特定的参数和次数);2) 定义它被调用时的行为。如果期望没有被满足(比如没被调用,或调用次数不对),测试会失败。

    using ::testing::_; using ::testing::Return; // 期望charge方法被调用恰好一次,参数可以是任意double值,调用时返回false EXPECT_CALL(mockGateway, charge(_)).Times(1).WillOnce(Return(false));

如何选择?

  • 如果你只关心模拟对象在被调用时做什么,而不关心它是否一定被调用或调用几次,用ON_CALL。这常用于配置一个“桩”(Stub),即一个只提供预设响应的简单替身。
  • 如果你要验证被测对象与依赖的交互协议(例如,“它必须调用一次登录接口,然后调用至少一次查询接口”),那么一定要用EXPECT_CALL。这是Mocking的精髓,用于验证对象间的协作。

3.3 匹配器(Matchers)与动作(Actions)

匹配器(Matchers)用于更精细地指定EXPECT_CALL中的参数预期。上面的_是一个通配符匹配器,匹配任何值。gmock提供了丰富的内置匹配器:

using ::testing::Eq; // 等于 using ::testing::Ge; // 大于等于 using ::testing::NotNull; // 不为空指针 using ::testing::StartsWith; // 字符串以...开头 // 期望logTransaction被调用,第一个参数以“TXN_”开头,第二个参数大于等于100.0 EXPECT_CALL(mockGateway, logTransaction(StartsWith("TXN_"), Ge(100.0)));

你还可以组合匹配器,甚至自定义匹配器,以满足复杂的参数验证需求。

动作(Actions)定义了方法被调用时具体要做什么。最常用的是Return,也可以做更多事:

using ::testing::Return; using ::testing::Throw; using ::testing::SetArgReferee; using ::testing::Invoke; // 返回固定值 EXPECT_CALL(mockGateway, charge(100.0)).WillOnce(Return(true)); // 抛出异常 EXPECT_CALL(mockGateway, charge(0.0)).WillOnce(Throw(std::invalid_argument("Amount zero"))); // 修改通过引用传递的参数(假设charge有一个bool& success参数) // EXPECT_CALL(mockGateway, charge(100.0, _)).WillOnce(SetArgReferee<1>(true)); // 调用一个自定义函数或lambda EXPECT_CALL(mockGateway, getTransactionId()).WillRepeatedly(Invoke([](){ static int id = 0; return "TXN_" + std::to_string(++id); }));

WillOnce表示一次调用的动作,WillRepeatedly表示后续所有调用的默认动作。你可以链式调用多个WillOnce来为连续的调用指定不同的行为。

3.4 验证调用次数与顺序

EXPECT_CALL后可以跟.Times()来指定期望的调用次数:

using ::testing::AnyNumber; using ::testing::AtLeast; // 调用任意次数(包括0次) EXPECT_CALL(mockGateway, logTransaction).Times(AnyNumber()); // 至少调用一次 EXPECT_CALL(mockGateway, charge).Times(AtLeast(1)); // 调用恰好两次 EXPECT_CALL(mockGateway, getTransactionId).Times(2);

默认情况下,gmock不关心多个EXPECT_CALL之间的顺序。如果你需要验证调用顺序,需要使用InSequence对象:

using ::testing::InSequence; { InSequence seq; // 此作用域内的EXPECT_CALL必须按声明顺序发生 EXPECT_CALL(mockGateway, charge(100.0)).Times(1); EXPECT_CALL(mockGateway, logTransaction).Times(1); // 必须在charge之后调用 }

4. 编写集成测试用例

现在,我们将所有知识串联起来,为一个使用了OrderProcessorPaymentGateway的完整场景编写测试。

首先,在tests/test_order_processor.cpp中:

#include <gtest/gtest.h> #include <gmock/gmock.h> #include "order_processor.h" #include "mock_payment_gateway.h" using ::testing::_; using ::testing::Return; using ::testing::Ge; using ::testing::StartsWith; // 定义一个测试夹具(Test Fixture) class OrderProcessorTest : public ::testing::Test { protected: void SetUp() override { // 在每个测试用例开始前,创建模拟对象和被测对象 mockGateway = new MockPaymentGateway(); // 使用原始指针便于gmock验证 processor = std::make_unique<OrderProcessor>(mockGateway); } void TearDown() override { // 在每个测试用例结束后,可以在这里清理。 // 注意:通常不需要手动delete,如果使用智能指针管理则更安全。 // 这里因为EXPECT_CALL验证需要,我们可能保持原始指针。 // 更好的做法是使用std::unique_ptr<MockPaymentGateway>,并通过.get()获取原始指针注入。 delete mockGateway; } // 使用原始指针是为了在测试中方便地对模拟对象设置期望。 // 在实际测试中,更安全的做法是使用std::unique_ptr来管理生命周期。 MockPaymentGateway* mockGateway; std::unique_ptr<OrderProcessor> processor; }; // 测试用例1:支付成功 TEST_F(OrderProcessorTest, ProcessOrder_Success) { Order order{/* ... */}; order.totalAmount = 150.0; // 设置期望:charge会被调用一次,参数为150.0,并返回true EXPECT_CALL(*mockGateway, charge(150.0)) .Times(1) .WillOnce(Return(true)); // 我们不关心logTransaction是否被调用,所以不设置EXPECT_CALL。 // 或者用Times(AnyNumber())表示调用任意次均可。 // 执行被测方法 bool result = processor->processOrder(order); // 验证结果 EXPECT_TRUE(result); // gmock会在测试结束时自动验证所有EXPECT_CALL是否满足。 } // 测试用例2:支付失败后重试成功 TEST_F(OrderProcessorTest, ProcessOrder_RetryAfterFailure) { Order order{/* ... */}; order.totalAmount = 200.0; // 设置期望:charge被调用两次,第一次返回false,第二次返回true EXPECT_CALL(*mockGateway, charge(200.0)) .Times(2) // 总共调用两次 .WillOnce(Return(false)) // 第一次调用返回false .WillOnce(Return(true)); // 第二次调用返回true bool result = processor->processOrder(order); EXPECT_TRUE(result); // 最终应该成功 } // 测试用例3:验证交互协议(调用顺序和参数) TEST_F(OrderProcessorTest, ProcessOrder_LogsTransactionOnSuccess) { Order order{/* ... */}; order.totalAmount = 99.99; // 模拟一个交易ID std::string mockTxId = "TXN_12345"; EXPECT_CALL(*mockGateway, charge(99.99)) .WillOnce(Return(true)); // 期望getTransactionId被调用,并返回我们模拟的ID EXPECT_CALL(*mockGateway, getTransactionId()) .WillOnce(Return(mockTxId)); // 期望logTransaction被调用,且第一个参数是我们返回的mockTxId,第二个参数是金额 EXPECT_CALL(*mockGateway, logTransaction(mockTxId, 99.99)) .Times(1); bool result = processor->processOrder(order); EXPECT_TRUE(result); } // 测试用例4:支付一直失败,返回false TEST_F(OrderProcessorTest, ProcessOrder_FailsAfterTwoAttempts) { Order order{/* ... */}; order.totalAmount = 50.0; // 设置期望:charge被调用两次,都返回false EXPECT_CALL(*mockGateway, charge(50.0)) .Times(2) .WillRepeatedly(Return(false)); // 两次调用都返回false bool result = processor->processOrder(order); EXPECT_FALSE(result); // 最终应该失败 }

代码解析与技巧:

  1. 测试夹具(TEST_F:我们定义了一个OrderProcessorTest类继承自::testing::Test。这允许我们在SetUpTearDown中为每个测试用例准备和清理公共环境(模拟对象和被测对象)。使用TEST_F宏来编写属于这个夹具的测试用例。
  2. 模拟对象生命周期:示例中使用了原始指针,并在TearDown中删除。这在简单情况下可行,但更好的实践是使用std::unique_ptr<MockPaymentGateway>来管理模拟对象的生命周期,在SetUp中创建,在夹具类析构时自动释放。注入时使用mockGateway.get()获取原始指针。
  3. 期望的精确性:在“重试成功”测试中,我们精确指定了charge会被调用两次,并且设定了两次不同的返回值。这完美验证了OrderProcessor::processOrder中的重试逻辑。
  4. 验证交互:在“验证交互协议”测试中,我们不仅验证了返回值,还验证了getTransactionIdlogTransaction的调用及其参数传递。这是单元测试中非常强大的部分,确保对象间协作符合设计预期。

5. 高级技巧与最佳实践

5.1 处理复杂依赖与多态

有时,你的类可能依赖一个接口,但该接口的方法返回或接收另一个复杂对象。gmock同样可以处理。

假设PaymentGateway有一个方法返回一个TransactionReport对象:

class TransactionReport { /* ... */ }; class PaymentGateway { public: virtual std::unique_ptr<TransactionReport> generateReport() = 0; };

在模拟时,你需要让模拟方法返回一个std::unique_ptr<TransactionReport>。你可以返回一个new出来的真实对象(如果测试需要),或者返回一个模拟对象(如果TransactionReport本身也很复杂)。gmock通过Return(ByMove(...))来支持返回move-only类型(如unique_ptr)。

#include <gmock/gmock.h> #include <memory> class MockPaymentGateway : public PaymentGateway { public: MOCK_METHOD(std::unique_ptr<TransactionReport>, generateReport, (), (override)); }; // 在测试中 auto mockReport = std::make_unique<TransactionReport>(); // ... 设置mockReport的一些状态(如果需要) EXPECT_CALL(*mockGateway, generateReport()) .WillOnce(Return(ByMove(std::move(mockReport))));

5.2 使用NiceMock、StrictMock与NaggyMock

gmock提供了三种不同严格程度的模拟对象包装器:

  • NiceMock<MockClass>:最宽松。对于没有设置EXPECT_CALL的调用,它会生成一个默认行为(返回默认构造值、void函数什么都不做等),并且不会产生任何警告。这可以让你只关注你感兴趣的交互,忽略无关调用。对于新手,我推荐从这个开始,可以减少测试噪声。
  • StrictMock<MockClass>:最严格。任何未预期的调用(即没有对应EXPECT_CALL的调用)都会导致测试立即失败。这有助于捕获多余的或意外的交互,但可能会让测试变得脆弱。
  • NaggyMock<MockClass>(默认):介于两者之间。未预期的调用会产生警告(gtest日志),但不会导致测试失败。

使用方式:

using ::testing::NiceMock; NiceMock<MockPaymentGateway> niceMock; // 未预期的调用会被安静地处理 StrictMock<MockPaymentGateway> strictMock; // 未预期的调用会导致测试失败 // MockPaymentGateway naggyMock; // 默认就是NaggyMock

5.3 模拟非虚函数与模板类

gmock主要设计用于模拟虚函数(多态)。对于非虚函数或自由函数,模拟起来比较困难,因为这通常需要链接期替换,涉及到链接器技巧或平台特定的功能(如Linux的LD_PRELOAD)。这不是gmock的推荐使用场景。更佳的做法是通过设计避免直接模拟非虚函数,例如将功能封装到一个带有虚接口的类中。

对于模板类,你可以为具体的模板实例创建模拟类。例如,你有一个Repository<T>模板,你可以为Repository<User>创建一个模拟类MockUserRepository,它继承自Repository<User>并模拟其方法。前提是这些方法是虚函数。

5.4 测试私有或受保护成员

单元测试的最佳实践是通过公共接口测试类。如果一段逻辑必须通过私有方法测试,那可能意味着这个逻辑应该被提取到另一个类中(单一职责原则)。如果由于历史原因或特殊情况必须测试私有成员,有几种方法(如友元测试类、将测试代码放在同一个编译单元),但这些都破坏了封装,应作为最后手段。

6. 常见问题与调试技巧

6.1 链接错误与未定义引用

这是集成gmock时最常见的问题。

  • 症状:编译成功,链接时报错,提示undefined reference totesting::...MockPaymentGateway` 的虚函数表。
  • 原因:测试目标没有正确链接gmock库。如2.1节所述,确保你的target_link_libraries包含了gmock。有时还需要链接gtestpthread(在Linux/macOS上)。
  • 解决:检查CMakeLists.txt,确保target_link_libraries(your_test_target PRIVATE gmock gtest [gtest_main])。如果使用pthread,添加find_package(Threads REQUIRED)并链接${CMAKE_THREAD_LIBS_INIT}

6.2 预期调用未被满足(Uninteresting call)

  • 症状:测试通过,但控制台输出大量警告,如[ UNINTERESTING CALL ] MockPaymentGateway::charge(100)
  • 原因:你的代码调用了模拟对象的某个方法,但你没有为这个调用设置任何EXPECT_CALLON_CALL。默认的NaggyMock会报告这种“不感兴趣”的调用。
  • 解决
    1. 检查是否遗漏了期望:如果这个调用是你测试逻辑的一部分,你应该为它添加EXPECT_CALL
    2. 使用NiceMock:如果这个调用是无关紧要的(例如,一个日志方法,不影响测试结果),你可以将模拟对象包装在NiceMock中,或者为这个方法设置一个宽松的默认行为:ON_CALL(mock, logMethod(_)).WillByDefault(Return());
    3. 审视设计:是否依赖了不需要的接口?能否简化?

6.3 预期调用次数不匹配

  • 症状:测试失败,错误信息类似Actual function call count doesn't match EXPECT_CALL(mock, method) ... Expected: to be called once; Actual: called 0 times.
  • 原因:你设置了EXPECT_CALL指定了调用次数(如Times(1)),但实际代码没有调用,或者调用了更多次。
  • 调试
    1. 仔细阅读错误信息,gmock会告诉你它期望什么,实际发生了什么。
    2. 在调试器中运行测试,在模拟方法处设置断点,查看调用栈,理解为什么调用没有发生或发生了多次。
    3. 检查你的测试逻辑和被测代码的逻辑是否一致。可能是条件分支(if/else)导致某些路径未执行。

6.4 参数匹配器不匹配

  • 症状:测试失败,信息提示Unexpected function call或参数不匹配。
  • 原因:实际调用方法时传入的参数,与你EXPECT_CALL中指定的参数匹配器(如Eq(100.0))不匹配。
  • 解决
    1. 使用更宽松的匹配器,比如_(任何值)或Gt(0)(大于0),先让测试通过。
    2. 在测试中打印出实际传入的参数值。你可以使用SaveArg动作来捕获参数供后续检查,或者临时修改代码添加日志。
    3. 检查被测对象的计算逻辑,确认它生成的参数是否正确。

6.5 在模拟对象的析构函数中验证期望

gmock会在模拟对象析构时,自动检查所有在其身上的EXPECT_CALL是否得到满足。这是一个非常重要的特性。但是,这也意味着模拟对象必须存活到测试结束。如果你在测试中途提前删除了模拟对象(比如在某个作用域内创建的局部unique_ptr),那么析构时的验证就会提前发生,可能无法捕获后续本应发生的调用。

最佳实践:让模拟对象的生命周期覆盖整个测试用例。通常将其作为测试夹具的成员变量,在SetUp中创建,在TearDown或夹具析构时销毁。

6.6 处理FLaky测试(不稳定的测试)

有时测试会间歇性失败,这可能与gmock无关,而是并发、未初始化状态或外部依赖导致的。但如果涉及gmock,常见原因有:

  • 期望过于严格:比如使用了StrictMock且未覆盖所有可能的调用路径。考虑改用NiceMock或补充ON_CALL
  • 调用顺序依赖:多个线程或不确定的执行顺序可能导致调用顺序与InSequence块中声明的顺序不一致。如果顺序不是业务关键,避免使用InSequence。如果必须,确保测试逻辑能产生确定的顺序。
  • 未清理的全局状态:gmock本身没有全局状态问题,但你的被测代码或模拟对象如果有静态变量,可能需要在SetUp/TearDown中重置。

集成GoogleMock到你的C++单元测试中,初期可能会觉得有些繁琐,但一旦掌握,它将极大地提升你测试复杂交互逻辑的能力和信心。记住,模拟的目的不是让测试通过,而是通过精确的控制和验证,让你对代码的行为有更深的理解和掌控。从简单的ON_CALL开始,逐步尝试EXPECT_CALL来验证交互,你会发现自己编写的代码也会因为更容易测试而变得更加模块化和清晰。