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
让我直接摆几个真实场景:
- 依赖不可控:比如你的代码调了一个支付接口,测试里你想验证「支付成功」和「支付失败」两个分支。总不能每次跑测试都去真的扣钱吧?
- 依赖速度慢:查询一个远程数据库,一次查询 2 秒,100 个测试就是 200 秒,你等得起吗?
- 环境不一致:本地开发环境可能没有某个服务,或者版本不对。我以前遇到过测试在 Jenkins 上跑不过,因为 Jenkins 的 Redis 版本跟本地不一样……
- 难以触发某些状态:比如网络超时、数据库连接池耗尽、第三方 API 返回 500。这些在真实环境里很难稳定复现,用 Stub 或 Mock 可以轻松模拟。
避坑指南: 我曾经在一个项目里过度使用 Mock,把整个业务逻辑都 Mock 掉了,结果跑测试全绿,上线就崩。记住:Mock 是用来隔离外部依赖的,不是用来掩盖设计问题的。
Mock 与 Stub 的区别
这两个最容易搞混。我个人的理解很简单:
| Stub(桩) | Mock(模拟) | |
|---|---|---|
| 核心目的 | 提供预设的返回值 | 验证交互行为(是否被调用、调用次数、参数) |
| 验证方式 | 不验证,只管返回 | 需要验证期望 |
| 举例 | 调用 get_user(id) 直接返回一个固定用户 | 验证 send_email() 被调用了 1 次且参数正确 |
| 使用场合 | 想让被测代码走某个条件分支 | 想确保某个函数被正确调用了 |
你想想看:Stub 就像考试时给答案的小抄——你只要结果;Mock 就像班主任的监控摄像头——你得盯着它有没有被触发。
常见坑: 很多新手用 Mock 去验证返回值,其实验证返回值是 Stub 的活。Mock 只关心「有没有按预期调用」,返回值该由断言来检查。
测试替身家族图谱
我把五种替身放在一张图里,方便你建立整体认知:
个人经验: 初学者先重点掌握 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 的区别以及使用场景。这些知识是后续章节的基础,后面的所有例子都会围绕这张图展开。
记住一句话:测试替身不是用来「欺骗」测试的,而是用来「隔离」外部依赖的。 用好了它就是你的左膀右臂,用不好就会变成混乱的源头。