Mock基础:什么是Mock、Mock与Stub的区别、Mock在测试中的角色

做接口测试的同学,多半都经历过这种尴尬:上游服务还没写好,下游服务动不动就挂,测试跑着跑着就卡住了。这时候就需要一位“替身演员”——Mock。说白了,Mock就是一个可以模拟真实对象的假对象,专门用来帮你把被测单元从外部依赖中解耦出来。

我个人最早接触Mock是在一个微服务改造项目中。那时团队要测支付模块,但第三方支付网关的沙箱环境三天两头出问题。我记得有次凌晨两点,测试同事在群里喊“网关又超时了”,整个CI流水线全红。从那之后,我下定决心要把Mock好好用起来——不依赖真实的网络,不依赖真实的数据库,测试环境才能稳定。

什么是Mock?

Mock,中文常翻译为“模拟对象”或“替身”。它的核心作用是:代替真实依赖,让你只专心测自己的代码逻辑。

先看一个最简单的例子。你写了一个用户注册服务,内部要调短信网关发验证码:

class UserService:
    def __init__(self, sms_client):
        self.sms_client = sms_client

    def register(self, phone, password):
        # 做校验...
        self.sms_client.send(phone, "验证码:1234")
        # 保存用户...

如果直接测这个类,你得配好短信网关地址、账号、密钥。而且在单测里真的发一条短信出去,既不经济也不可靠。这时候我就用Mock来替换sms_client:

from unittest.mock import Mock

def test_register():
    sms_mock = Mock()
    service = UserService(sms_mock)

    service.register("13800138000", "abc123")

    # 验证是否调用了 send
    sms_mock.send.assert_called_once_with(
        "13800138000", "验证码:1234"
    )

看到没?我们没有真的发短信,只是检查了“有没有用正确的参数调用过send”。这就够了——因为短信发送的逻辑不是当前测试的重点,重点在于注册流程的正确性。

一句话总结:Mock让你专注测试“现在要测的东西”,而不用操心“别人的东西”是否正常。

Mock vs Stub:差别在哪?

很多新手容易搞混这两个概念。我见过好几个项目,代码里混着用,测试可读性很差。咱们一次性说清楚。

维度 Stub Mock
核心目标 提供预定义的数据(状态验证) 验证行为是否发生、调用顺序、次数(行为验证)
典型用法 返回固定值,比如stub.get_user() -> {"name":"张三"} 记录调用历史,然后断言mock.method.assert_called_once()
是否需要断言 通常不需要对Stub本身做断言,只对被测代码的返回值做断言 必须对Mock对象做断言,验证交互是否正确
举个例子 模拟数据库返回一条用户记录 验证“通知服务”的notify()被调用了几次

我以前带过一个新人,他写测试时总喜欢在Mock对象上调用return_value,然后又不做任何assert_xxx,测试跑完也不知道到底有没有调用。那就是把Mock当Stub用了——功能上也能跑,但失去了Mock最重要的意义:验证行为。

我的习惯:如果只需要返回假数据,用Stub;如果还需要确保方法“被调用过”“被调用了3次”“带特定参数调用了”,就用Mock。一个测试里可以Stub和Mock混着用,但你要清楚各自扮演什么角色。
曾经踩过的坑:我在一个CI流水线里用Mock模拟了外部API,但因为Mock配置了错误的side_effect,导致测试通过但实际API调用根本不会触发重试逻辑。后来排查了一整天才发现——Mock把异常吞了。所以用Mock时,要确认它的行为足够接近真实场景。

Mock在测试中的角色

Mock在测试生态里到底扮演什么角色?我给它归纳了三个核心角色:

  1. 隔离器——切断被测代码对外部依赖(网络、数据库、文件系统、第三方服务)的直接联系,保证测试只运行在内存中,速度快、结果稳定。
  2. 行为记录仪——Mock会记住所有调用细节:调用了哪些方法、传入了什么参数、调用了多少次、顺序如何。你可以基于这些记录做精确断言。
  3. 场景模拟器——通过side_effect模拟异常、超时、返回特定错误码等边界情况。这在真实环境下极难触发,但用Mock一行代码就能搞定。

你想想看,如果没有Mock,怎么模拟支付接口返回“余额不足”?你得到数据库里先把用户余额改掉,再调真实接口——万一没回滚干净,线上数据就乱了。用Mock:mock_pay.side_effect = InsufficientBalanceError,干净利落。

一句话总结:Mock是测试的“沙盒”,让你在可控的环境里,把被测代码的每一根神经都摸清楚。

知识体系:Mock的三层认知

为了帮你快速建立整体认知,我画了一张结构图。这是本章最核心的框架,建议你盯着看两分钟。

Mock 基础知识体系 什么是Mock 替身对象 · 解耦依赖 · 轻量快速 Mock vs Stub 行为验证 vs 状态验证 Mock的三大角色 隔离 · 记录 · 模拟 Mock 验证方式 · assert_called_once() · assert_called_with() · assert_any_call() / 调用计数 Stub 常见用法 · return_value = 固定数据 · side_effect = 异常或迭代值 · 不关心调用次数,只关心返回值 隔离器 去除网络/数据库依赖 测试稳定、快如闪电 行为记录仪 记录每次调用细节 精确断言调用参数次数 场景模拟器 模拟异常/超时/错误码 覆盖正常测不到的边界 💡 核心原则 写测试时先问自己:我要验证的是“状态”还是“行为”?——答案决定你用 Stub 还是 Mock。

这张图从左到右梳理了本章的核心脉络。你注意看,Mock与Stub的关键区别在于验证的目标——一个盯行为,一个盯状态。而Mock在测试中的三大角色,正好对应了我们日常写单测的核心需求:解耦、记录、模拟。

什么时候不该用Mock?

说完了优点,得泼盆冷水。Mock不是万能的。我自己总结过三条红线:

  • 不要Mock自己当前测试的核心逻辑。比如你在测一个加密算法,把加密函数Mock掉——那这个测试就毫无意义了。
  • 不要过度Mock。如果一个测试里Mock了七八个对象,代码读起来很痛苦,而且一改接口全崩。这时候不如考虑集成测试或写一个更精简的接口抽象层。
  • 不要用Mock代替契约测试。Mock只能模拟调用签名,但无法保证真实服务的行为是否一致。如果你和第三方服务有接口约定,建议配合契约测试(如Pact)来证实双方的理解一致。
我曾经踩过的第二个坑:在一个订单服务里,我把数据库层全部Mock了,然后测了各种业务逻辑,测试全绿。上线当天,真实数据库的某个索引慢到导致超时——因为Mock里的“假数据库”永远不慢。从那以后,我对“数据库操作”这一层只用Mock验证调用次数,但一定会额外写一个轻量级的集成测试来检查SQL是否真的能跑。

小结

Mock是什么?一个替身对象,帮你从外部依赖中解脱出来。Mock和Stub怎么分?一个验行为,一个给数据。Mock在测试中做什么?隔离、记录、模拟——三个角色缺一不可。