Mock与Stub的核心区别:验证行为 vs 提供数据

说实话,刚接触测试替身那会儿,我也搞不清 Mock 和 Stub 到底有什么不同。当时写了个单元测试,用 Mock 对象把结果硬编码返回,然后写了一大堆 assert_called_once —— 后来被 code review 怼了:「你这是在测实现,不是测行为。」

嗯,那次之后我才真正理解了:Mock 和 Stub 虽然长得像,但它们的核心职责完全不同。一个盯着「你怎么调用的」,另一个只管「你调用我给你啥」。

从定义看区别:行为 vs 数据

咱们先看一个最直白的对比表格,你大概扫一眼就能抓住核心。

维度MockStub
核心关注点验证交互行为(是否被调用、调用次数、参数)提供预设返回数据(屏蔽外部依赖)
会不会让测试失败会 —— 如果期望的调用没发生,测试就挂不会 —— 即使从未调用,测试照样通过
典型场景验证支付服务是否调用了发送通知的接口让数据库查询直接返回一批测试数据
实现方式需要记录调用历史 + 支持断言只需要覆盖方法返回值,不需要「记忆」

说白了,Stub 就像个工具人,你吩咐它「每次问张三的余额,都返回 100」,它照做就行,不关心张三存不存在。Mock 则像个监视器,它不光返回数据,还把你每次怎么调用的都记录下来,然后告诉你「不对,你应该先查余额再扣款,你顺序错了」。

使用场景决定选择

我个人习惯是:如果测试要验证「某个方法有没有被正确调用」,我就用 Mock;如果只是想跳过真实依赖(比如网络、数据库)的干扰,用 Stub 就够了。

举个例子,测试一个订单状态更新的功能:

  • 用 Stub —— 让仓储接口直接返回「订单存在且状态为已支付」,测试业务逻辑的分支。
  • 用 Mock —— 验证当订单超时时,是否确实调用了「发送催付短信」的方法,并且传参正确。

你看,两种替身解决的痛点完全不同。

验证行为:Mock 的职责

Mock 的核心能力是「记性好」。它会在内部维护一个调用记录表,你可以在测试最后检查:


# Python 示例(使用 unittest.mock)
from unittest.mock import Mock

sms_service = Mock()

order = Order(sms_service)
order.mark_as_paid()

# 验证 sms_service.send 被调用了一次,且参数为 (user_id="123")
sms_service.send.assert_called_once_with(user_id="123")

如果 mark_as_paid 里面没有调用 sms_service.send,或者传的参数不对,这个断言就会抛出异常,测试失败。这就是「验证行为」的威力。

关键点: Mock 的重点是「如何被调用」,而不是「返回什么」。即使 Mock 的返回值被忽略,你照样可以写断言。

提供数据:Stub 的职责

Stub 就简单多了:它不关心你怎么调,只保证「你问数据我给你」。在 Python 里,最朴素的 Stub 就是一个普通对象:


class UserStub:
    def get_name(self, user_id):
        if user_id == 1:
            return "张三"
        elif user_id == 2:
            return "李四"
        return None

def test_greeting():
    stub = UserStub()
    greeting = generate_greeting(stub, 1)
    assert greeting == "你好,张三"
    # 这里绝不会去检查 stub.get_name 是否被调用了

在我的项目里,经常用一个 Stub 替代 Redis 缓存,直接返回预设的热点数据,测试跑起来飞快,还不用启动 Redis 服务。注意:Stub 本身不会记录调用情况,所以你不能断言它被调用了多少次 —— 想断言?那就得用 Mock 了。

小技巧: Python 的 unittest.mock.Mock 其实同时具备 Mock 和 Stub 的功能。但为了职责清晰,我建议如果你只需要 Stub,就用 MagicMock 并且不写任何断言;或者干脆自建一个普通类,避免滥用 Mock 的「记录能力」导致测试难以阅读。

常见误区与避坑指南

光讲理论不够,我来分享几个我踩过的坑:

  • 误区:把所有替身都叫 Mock。 曾经在团队里,大家把所有测试替身统称为「mock」,导致 code review 时有人说「你这个 mock 没有做断言」,但实际上它只是个 Stub……沟通成本直线上升。现在我都会明确区分术语。
  • 误区:用 Mock 代替 Stub 做数据提供。 比如直接用 mock_db.query.return_value = [...],然后后面再加一堆 assert_called_with,把测试搞得又长又脆。如果只是需要数据,请用 Stub。
  • 我曾经 在重构一个支付模块时,把 Stub 改成 Mock 去验证内部的缓存命中逻辑,结果几十个测试因为调用顺序变了而全红。后来发现:我根本不需要验证内部如何调用缓存,只要数据对了就行。改成 Stub 后测试直接稳定。

注意: 不要用 Mock 去验证外部系统的实现细节。Mock 验证的是「你和我之间的协议(接口调用)」,不是「你内部怎么处理」。比如测试订单服务时,你验证它调用了短信服务,但不要验证短信服务内部是否又去调了什么日志接口 —— 那是短信服务的测试。

一张图看懂核心区别

下面这张图总结了 Mock 和 Stub 在思维模型上的根本差异:一个面向交互记录,一个面向数据替代。

Mock vs Stub 核心对比图 Mock(行为验证) 记录每次调用(参数、次数、顺序) 通过断言验证交互是否符合预期 测试失败 = 调用行为不匹配 Stub(数据提供) 返回固定值 / 预设数据 替代真实依赖(数据库、网络、文件系统) 不验证调用,不记录历史 核心区别 一句话总结 Mock: “你调对了没有?” — 验证行为 Stub: “你要的数据在这” — 提供数据

画完图再回头看,你会发现 Mock 和 Stub 就像一枚硬币的两面:Mock 面向「交互协议」,Stub 面向「数据契约」。选哪个取决于你要验证什么 —— 如果你关心这个动作是否发生,用 Mock;如果你只关心数据对不对,用 Stub。简单吗?用对了才能写出干净、可维护的测试。