如何在多线程测试场景中安全使用 GoogleMock mock 对象
2026/9/12 16:05:44 网站建设 项目流程

如何在多线程测试场景中安全使用 GoogleMock mock 对象

【免费下载链接】googletestGoogleTest - Google Testing and Mocking Framework项目地址: https://gitcode.com/GitHub_Trending/go/googletest

当被测代码本身是多线程的——例如事件在后台线程上派发、多个线程并发调用同一个依赖——单元测试里的 GoogleMock mock 对象就会被多个线程同时访问。googletest 仓库的 gMock Cookbook 在 "Using gMock and Threads" 一节明确给出了一套规则:只要区分清楚 mock 使用流程中哪些步骤必须独占访问、哪些步骤可以由 gMock 自己加锁保护,mock 和线程就能安全共存;违反这些规则(比如在另一个线程正在调用 mock 方法时设置期望)会得到未定义行为(undefined behavior)。本文基于项目文档给出完整的操作路径和验证方式。

先对照 mock 使用流程,判断每个步骤能开几个线程

gMock Cookbook 回顾了一个 mock 的标准使用步骤:

  1. 创建 mock 对象foo
  2. ON_CALL()EXPECT_CALL()设置默认动作和期望;
  3. 被测代码调用foo的方法;
  4. 可选地验证并重置 mock;
  5. 由你自己或代码销毁 mock,析构函数会自动验证剩余期望。

文档要求把测试代码(区别于被测代码)放在一个线程里执行,并给出每一步的线程约束:

  • 步骤 1(创建 mock)不需要任何锁;
  • 步骤 2(设置默认动作和期望)与步骤 5(销毁)必须保证没有其他线程正在访问该 mock
  • 步骤 3 和 4 可以在一个或多个线程里执行——gMock 负责加锁,测试代码不需要额外处理,除非测试逻辑本身要求。

因此操作路径是:先在主线程创建 mock 并设完全部期望,再把 mock 交给被测代码让多个线程并发调用,最后在没有其他线程访问时做验证或让它析构。文档明确警告:如果在另一个线程调用 mock 方法的同时设置期望,行为未定义,"That's not fun, so don't do it"。

另外注意,gmock_for_dummies 中的 Important note 还强调:gMock 要求期望必须在 mock 函数被调用之前设置,不能把EXPECT_CALL()与对 mock 方法的调用交错执行,也不能在把 mock 交给被测 API 之后再设置期望,否则行为未定义。

理解 action 在多个线程中的执行方式

理解这一点才能正确设计并发测试:

  • gMock 保证 mock 函数的 action 在调用该 mock 函数的同一个线程中执行。文档给出的例子:
EXPECT_CALL(mock, Foo(1)) .WillOnce(action1); EXPECT_CALL(mock, Foo(2)) .WillOnce(action2);

如果Foo(1)在 thread 1 被调用、Foo(2)在 thread 2 被调用,gMock 会在 thread 1 执行action1、在 thread 2 执行action2

  • gMock不会对不同线程中执行的 action 强加顺序(强加顺序可能造成死锁,因为各 action 之间可能需要协作)。所以上例中action1action2的执行可能交错;如果这会造成问题,文档建议的正确做法是在action1action2里添加适当的同步逻辑,让测试自身线程安全,而不是指望 gMock 排序。

把异步调用变成确定性的等待

如果多线程问题来自异步行为(例如被测类把事件放到后台线程派发),文档不建议插入sleep()碰运气,而是用 gMock action 加Notification对象强制异步测试同步化。gMock Cookbook 中 "Testing Asynchronous Behavior" 给出的示例(依赖示例中使用的absl::Notification):

class MockEventDispatcher : public EventDispatcher { MOCK_METHOD(bool, DispatchEvent, (int32), (override)); }; TEST(EventQueueTest, EnqueueEventTest) { MockEventDispatcher mock_event_dispatcher; EventQueue event_queue(&mock_event_dispatcher); const int32 kEventId = 321; absl::Notification done; EXPECT_CALL(mock_event_dispatcher, DispatchEvent(kEventId)) .WillOnce([&done] { done.Notify(); }); event_queue.EnqueueEvent(kEventId); done.WaitForNotification(); }

做法是:照常设置期望,再用WillOnce追加一个动作去done.Notify();主线程随后调用WaitForNotification()等待后台线程完成这次 mock 调用,测试结束时可以安全退出。文档同时指出这个模式的缺点:如果期望没有被满足,测试会一直等下去,最终靠超时失败,但调试起来更慢。缓解方式是把WaitForNotification()换成WaitForNotificationWithTimeout(ms)

在多线程测试中验证期望

mock 析构时会自动验证所有期望是否满足,不满足会生成 GoogleTest 失败。例如期望未被满足时的失败信息形如(文档示例):

path/to/my_test.cc:119: Failure Actual function call count doesn't match this expectation: Actually: never called; Expected: called at least once. Stack trace: ...

但如果在多线程场景中 mock 由被测代码持有、无法保证最终被销毁(或被测代码有 bug 忘了 delete),自动验证就不可靠。这时按 gMock Cookbook "Forcing a Verification" 和 gMock Cheat Sheet 的说明,在主线程显式强制验证:

TEST(MyServerTest, ProcessesRequest) { using ::testing::Mock; MockFoo* const foo = new MockFoo; EXPECT_CALL(*foo, ...)...; // ... other expectations ... // server now owns foo. MyServer server(foo); server.ProcessRequest(...); // In case that server's destructor will forget to delete foo, // this will verify the expectations anyway. Mock::VerifyAndClearExpectations(foo); } // server is destroyed when it goes out of scope here.

两个函数都返回bool,仅当验证成功时为trueMock::VerifyAndClearExpectations(&mock_obj)只验证并移除期望,Mock::VerifyAndClear(&mock_obj)还额外移除ON_CALL()设置的默认动作。Cheat Sheet 建议把调用包进ASSERT_TRUE(),这样验证失败时不必继续执行后续逻辑。如果 mock 确实可能被泄漏且不需要验证,Cheat Sheet 还给出了Mock::AllowLeak(&mock_obj)

这里有一条必须遵守的限制:验证并清空之后不要再设置新的期望。在已经运行过 mock 的代码之后再设置期望属于未定义行为,gMock Cookbook 和 Cheat Sheet 都对此有明确说明。

限制与排查手段

  • DefaultValue<T>是全局资源,会影响程序中所有存活的 mock 对象。文档明确:不要从多个线程修改它,也不要还有 mock 在活动(in action)时去动它。
  • 调试期望匹配问题时,用--gmock_verbose=info运行测试:gMock 会打印每个ON_CALL/EXPECT_CALL的设置日志、每次 mock 调用的匹配情况、参数值和返回值的栈跟踪(文档示例输出中可以看到EXPECT_CALL(mock, F(_, _)) invokedMock function call matches ...这类记录)。测试代码内也可以用::testing::FLAGS_gmock_verbose = "error";调整该标志;如果栈帧太多,可以用--gtest_stack_trace_depth=max_depth限制数量。
  • gMock FAQ 还解释了一个多线程相关现象:如果测试崩溃且进程里有很多深栈的线程,失败信号处理器会记录大量信息(栈跟踪、地址映射等),ScopedMockLog拦截到这些不匹配期望的日志后会为每一条打印错误。文档说明这通常不是 gMock 的 bug,可以通过放宽期望(例如对非本测试产生的日志行Times(AnyNumber()))让测试更健壮。

操作路径小结

按文档规则执行:主线程创建 mock 并设完期望(此时无其他线程访问)→ 被测代码多线程调用 mock 方法,action 在各自调用线程执行、必要时在 action 内自行同步 → 所有线程停止访问后,靠析构自动验证,或对堆上 mock 用Mock::VerifyAndClearExpectations/Mock::VerifyAndClear显式验证并用ASSERT_TRUE检查返回值。异步派发场景用Notification加超时等待替代sleep()。只要每一步落在文档允许的线程约束内,验证失败信息("Actual function call count doesn't match..." 这类输出)就是判断期望是否被正确满足的依据。

【免费下载链接】googletestGoogleTest - Google Testing and Mocking Framework项目地址: https://gitcode.com/GitHub_Trending/go/googletest

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询