1. 测试替身概述:什么是测试替身,为什么需要 Mock 和 Stub

写单元测试的时候,最头疼的往往不是测自己的代码,而是被测代码调了一堆外部依赖——数据库、网络请求、第三方 API、文件系统……

这些依赖要是在测试环境里一调用就报错,或者状态不可控,那还测个啥?

所以就有了测试替身(Test Double)。说白了,就是用一些「假的」对象来代替那些真实的、不好搞的依赖。我刚开始做项目时总觉得写几个假对象很麻烦,直到有一次上线前跑测试,数据库连不上,整个 CI 挂了三天……后来我学乖了,该上替身上替身。

核心概念: 测试替身是一个统称,包括 Dummy、Fake、Stub、Mock、Spy 五种常见类型。

 什么是测试替身

测试替身(Test Double)这个术语最早由 Gerard Meszaros 提出,用来表示任何在测试中替代真实对象的对象。就像电影里的替身演员——动作危险、场景复杂时,找个长得像的人去演。

我习惯把它们分成两类:

  • 模拟行为类:Stub(桩)和 Mock(模拟),我们课程的重点。
  • 占位/简化类:Dummy(哑对象)、Fake(假实现)、Spy(间谍)。

嗯,这里要注意:很多人把 Mock 和 Stub 混着叫,其实它们用途完全不同。

 为什么需要 Mock 和 Stub

让我直接摆几个真实场景:

  1. 依赖不可控:比如你的代码调了一个支付接口,测试里你想验证「支付成功」和「支付失败」两个分支。总不能每次跑测试都去真的扣钱吧?
  2. 依赖速度慢:查询一个远程数据库,一次查询 2 秒,100 个测试就是 200 秒,你等得起吗?
  3. 环境不一致:本地开发环境可能没有某个服务,或者版本不对。我以前遇到过测试在 Jenkins 上跑不过,因为 Jenkins 的 Redis 版本跟本地不一样……
  4. 难以触发某些状态:比如网络超时、数据库连接池耗尽、第三方 API 返回 500。这些在真实环境里很难稳定复现,用 Stub 或 Mock 可以轻松模拟。
避坑指南: 我曾经在一个项目里过度使用 Mock,把整个业务逻辑都 Mock 掉了,结果跑测试全绿,上线就崩。记住:Mock 是用来隔离外部依赖的,不是用来掩盖设计问题的。

 Mock 与 Stub 的区别

这两个最容易搞混。我个人的理解很简单:

   Stub(桩)Mock(模拟)
核心目的提供预设的返回值验证交互行为(是否被调用、调用次数、参数)
验证方式不验证,只管返回需要验证期望
举例调用 get_user(id) 直接返回一个固定用户验证 send_email() 被调用了 1 次且参数正确
使用场合想让被测代码走某个条件分支想确保某个函数被正确调用了

你想想看:Stub 就像考试时给答案的小抄——你只要结果;Mock 就像班主任的监控摄像头——你得盯着它有没有被触发。

常见坑: 很多新手用 Mock 去验证返回值,其实验证返回值是 Stub 的活。Mock 只关心「有没有按预期调用」,返回值该由断言来检查。

 测试替身家族图谱

我把五种替身放在一张图里,方便你建立整体认知:

测试替身(Test Double)家族 测试替身 Dummy(哑对象) Fake(假实现) Stub(桩) Mock(模拟) Spy(间谍) 💡 本课程聚焦 Mock 和 Stub 它们是最常用的测试替身,也是“测试替身”的核心
个人经验: 初学者先重点掌握 Stub 和 Mock,其他三种了解一下就行。Dummy 只是占位(比如函数参数必须传但用不到),Fake 是轻量级实现(比如用内存数据库替代 MySQL),Spy 其实是对真实对象的观察——你可以在真实对象上附加统计信息,我项目里很少用,因为 Mock 框架自带 spy 功能。

 什么时候该用测试替身

我总结了一套判断标准:

  • 用替身: 依赖涉及网络、磁盘、大型库、状态可变的外部系统。
  • 不用替身: 纯函数、小型值对象、简单的数据容器——这些用真实对象反而更可靠、更易读。
  • 慎用替身: 当你发现测试里 Mock 了一堆自己写的内部函数时,可能是在暗示你代码需要重构。我吃过这个亏,后来把 Mock 对象控制在模块边界上,测试就好写多了。

举个简单的 Stub 例子(Python):

# 被测代码:从数据库获取用户名字,返回欢迎语
def greet_user(user_id, db_connector):
    user = db_connector.get_user(user_id)   # 依赖外部
    return f"Hello, {user['name']}"

# 测试中我们不需要真的数据库,用 Stub 代替
class StubDbConnector:
    def get_user(self, user_id):
        return {"name": "测试小明"}

def test_greet_user():
    stub = StubDbConnector()
    result = greet_user(42, stub)
    assert result == "Hello, 测试小明"

再看一个 Mock 的例子:

from unittest.mock import Mock

def test_notify_user():
    email_service = Mock()
    notify_user(user_id=1, email_service=email_service)
    # 验证 email_service.send 被调用了一次,且参数包含 user_id=1
    email_service.send.assert_called_once_with(user_id=1)
关键区别: Stub 只负责「提供数据」,Mock 负责「验证行为」——不要混淆,否则测试会变得很脆弱。

好了,这一章我们理清了测试替身的概念、为什么需要它们、Mock 和 Stub 的区别以及使用场景。这些知识是后续章节的基础,后面的所有例子都会围绕这张图展开。

记住一句话:测试替身不是用来「欺骗」测试的,而是用来「隔离」外部依赖的。 用好了它就是你的左膀右臂,用不好就会变成混乱的源头。