什么是Mock?——从一次“假”集成说起

聊到测试,很多人第一反应就是「写单元测试,把真实环境跑一遍」。但现实很骨感——你调用的第三方支付接口动不动就限流,数据库里永远缺几条脏数据,微服务间网络抖动让你测试跑得比生产还慢。这时候,就需要「假」的东西上场了。

我最早接触Mock是在一个电商项目里。当时要测订单结算流程,每次跑测试都得真的去扣库存、调支付网关。有一次线上支付网关临时维护,整个CI流水线挂了半天。后来我把支付模块Mock掉,测试速度从3分钟降到20秒。嗯,从那以后,我再也不敢裸测外联服务了。

Mock对象:模拟行为,验证交互

Mock的核心思想很简单:创建一个「替身」对象,让它按照你的脚本返回预设结果。测试时只关心被测代码和这个替身之间的交互对不对,而不关心替身内部逻辑。

# 假设你有一个支付网关客户端
class PaymentGateway:
    def charge(self, amount):
        # 真的去调远程API
        pass

# 测试时用Mock替代真实网关
mock_gateway = Mock()
mock_gateway.charge.return_value = {"status": "success"}
order_service = OrderService(mock_gateway)
result = order_service.checkout(100)
assert result["status"] == "success"
# 验证charge是否被调用了一次
mock_gateway.charge.assert_called_once_with(100)

你看,Mock不但可以返回假数据,还能记住「谁调了我」「传了什么参数」「调了几次」。这些验证能力是Mock独有的。

我的习惯: 优先用Mock验证「外部调用」是否正确,比如调了REST API、发了MQ消息、写了日志——这些交互一旦错位,查起来特别痛苦。

Stub:提供数据,不问过程

Stub和Mock很像,但侧重点不同。Stub只负责「给数据」,它不关心你调没调,调了几次——它就是个沉默的数据源。

# Stub:永远返回固定值
class UserServiceStub:
    def get_user(self, user_id):
        return {"id": 1, "name": "Alice", "bal": 100}

user_service = UserServiceStub()
result = some_business_logic(user_service)
# 测试只关心逻辑,不关心user_service是否被调了多次

我在项目中经常用Stub来替代那些「只读」的外部依赖——比如配置中心、字典表、静态数据服务。它们没有写操作,验证交互意义不大,给个固定值就够了。

Fake:轻量实现,真逻辑假数据

Fake是三者中最「实诚」的——它内部有真正的业务逻辑,但数据存储被简化了。比如用内存HashMap模拟数据库,用本地文件系统模拟云存储。

# Fake:用dict模拟数据库
class FakeUserRepository:
    def __init__(self):
        self._store = {}
    def save(self, user):
        self._store[user.id] = user
    def find_by_id(self, user_id):
        return self._store.get(user_id)

def test_user_save():
    repo = FakeUserRepository()
    service = UserService(repo)
    service.create_user({"id": 1, "name": "Bob"})
    assert repo.find_by_id(1).name == "Bob"

回想有一次要测复杂的业务聚合逻辑,涉及多条记录的事务。用Mock和Stub根本覆盖不了数据状态流转,最后我写了一个Fake仓库,用内存表模拟了所有CRUD操作。测试跑得飞快,而且中途重构了存储层,Fake代码几乎没动——因为它复用了真实接口的逻辑骨架。

三者对比:一张表说清

特性 Mock Stub Fake
核心目的 验证交互行为 提供预设数据 轻量实现真实逻辑
是否验证调用次数 是(必须) 否 否(但可通过辅助方法验证)
内部是否有真实逻辑 否(只有模拟行为) 否(只有返回值) 是(简化版实现)
典型场景 外部API、消息队列、日志 配置中心、静态数据 数据库、文件系统、缓存
维护成本 中等(需跟随接口变化) 低 较高(需维护伪实现)
适合的测试类型 单元测试、集成测试 单元测试 集成测试、契约测试

概念关系与选择指南

一句话总结:
Mock管交互,Stub管数据,Fake管逻辑。
当你需要验证“调没调对”→ 用Mock;只需要“给个值别报错”→ 用Stub;需要“让测试跑起来且带真实逻辑”→ 用Fake。
我曾经踩过的坑: 过多使用Mock会导致测试与实现强耦合。改一次内部调用参数,就碎一片测试。后来我学乖了:
• 对外部系统接口用Mock(因为它确实不稳定)
• 对自己的模块间交互尽量用Fake(因为重构时只改Fake不改变量)
• Stub只用于那些“永远不变”的配置值——否则你会在几十个测试里分别维护重复的Stub,痛苦。
Mock / Stub / Fake 关系与选择决策树 被测系统(待测试) Mock Stub Fake 特点 • 验证调用次数、参数 • 无内部逻辑 • 适合:外部API、MQ 特点 • 只返回固定数据 • 不验证交互 • 适合:配置、静态数据 特点 • 有简化版业务逻辑 • 数据存储内存化 • 适合:DB、文件系统 选择建议 需要验证交互 → Mock|只需要数据 → Stub|需要真实逻辑跑测试 → Fake

Mock回放在CI中的价值

你想想看,CI流水线每次提交都去跑真实环境——等第三方返回,等数据库建表,等消息队列确认……整个流程能慢到什么程度?我见过一个项目的CI全量测试跑了2小时,其中80%在等待外部依赖。

用了Mock回放之后:录制一次真实请求-响应对,然后在每次CI运行时直接用录制好的数据「回放」给被测系统。这样速度提升了,而且确定性极强——再也不用担心测试因为网络抖动而红了。

说白了,Mock回放就是「用一场录好的戏来代替现场直播」。而理解Mock、Stub、Fake的区别,是搭建回放方案的第一块基石。

记忆口诀:
Mock像警察,查谁打过电话;
Stub像仓库,只管给货不管查;
Fake像小卖部,麻雀虽小五脏全。