3. Stub基本概念:定义、工作原理、与Mock的区别、适用条件
Stub(桩)是最简单的测试隔离手段之一。它的核心思想是:用一个轻量级的替身对象替换真实依赖,替身只返回预设好的固定数据,不包含任何验证逻辑。Stub不关心被测代码是否调用了它、调用了多少次,只负责“给数据”——让测试能绕过外部依赖(如数据库、网络服务、文件系统)运行。
3.1 定义与核心特征
- 定义:Stub是测试替身的一种,它硬编码了内部方法的返回值,使被测单元在隔离环境下获得可控输入。
- 只出不验:Stub只提供预设响应,不记录调用次数、顺序、参数等交互信息。
- 状态隔离:Stub通常不维护内部状态(除预设数据外),每次调用返回相同结果。
- 轻量实现:无需模拟框架,手工类、匿名函数或简单配置即可创建。
3.2 工作原理
Stub的工作遵循“替换→预设→执行→忽略”四步流程:
- 替换:在测试准备阶段,用Stub对象替换被测单元的真实依赖(例如:将数据库连接接口替换为返回固定用户的Stub)。
- 预设:为Stub的方法分配固定的返回值或异常。
- 执行:运行被测代码,代码调用Stub方法,Stub直接返回预设数据,不执行真实依赖的逻辑。
- 忽略:测试结束,不需要验证Stub是否被调用;只验证被测单元的输出或状态。
关键点:Stub的“不验证”特性使其最纯粹——它承担的是“数据提供者”角色,而非“行为验证者”。
3.3 Stub 与 Mock 的本质区别
| 维度 | Stub | Mock |
|---|---|---|
| 定义目标 | 提供预设返回值,隔离外部依赖 | 验证交互行为(调用次数、参数顺序) |
| 是否验证调用 | 否 | 是(核心职责) |
| 返回值性质 | 固定、硬编码 | 可动态计算,但仍以验证交互为主 |
| 创建复杂度 | 极低(手工类/匿名函数即可) | 较高(通常需要模拟框架) |
| 典型应用场景 | 读取配置、查询数据库、调用外部API(只关心结果) | 验证日志记录、事件推送、缓存写入的次数与顺序 |
3.4 适用条件
- 依赖只提供数据:当真实外部系统仅返回数据(如天气API、用户详情查询),且测试不需要验证“如何调用”,只关注被测单元对数据的处理逻辑时,Stub是最佳选择。
- 依赖运行代价高:如需要启动真实数据库、调用第三方付费接口、或有显著延迟的远程服务,使用Stub可极大提升测试速度。
- 测试中不需要验证交互:如果测试目的是检验单元的业务逻辑,而非确认它调用了某个依赖多少次,那么Stub比Mock更简洁、不易误判。
- 向后兼容场景:当依赖接口变化频繁,但测试只关心输入输出转换时,Stub可以快速适配而不影响测试逻辑。
慎用场景:如果测试需要验证“日志是否发送了三次”或“支付消息是否以正确参数推送”,则应使用 Mock 而非 Stub,因为 Stub 不记录调用历史。
3.5 Stub 隔离流程图
3.6 代码示例:Python 手工 Stub
# 被测单元:根据用户ID获取用户名并拼接欢迎信息
class WelcomeService:
def __init__(self, user_repo):
self.user_repo = user_repo
def greet(self, user_id):
user = self.user_repo.get_user(user_id)
return f"Hello, {user['name']}!"
# 手工 Stub:替换真实数据库访问
class UserRepoStub:
def get_user(self, user_id):
# 注意:无论 user_id 是多少,都返回固定数据
return {"name": "Alice", "email": "alice@example.com"}
# 测试用例
def test_greet_with_stub():
stub = UserRepoStub()
service = WelcomeService(stub)
result = service.greet(123)
assert result == "Hello, Alice!" # 只验证输出,不验证 Stub 是否被调用
3.7 常见错误与优化
- 错误1:在 Stub 中加入断言 —— 导致 Stub 隐含了验证逻辑,破坏了“只提供数据”的原则,使测试意图模糊。
- 错误2:Stub 内部依赖复杂状态 —— 如引入计数器、计时器,使返回结果受调用次数影响,导致测试不可重复。
- 优化建议:Stub 尽量保持纯函数特性——相同的预设输入总是返回相同的数据;若需要不同的数据组合,使用多个 Stub 实例而非在内部切换逻辑。
调试技巧:在 Stub 中临时打印调用日志(不要断言)可以帮助定位被测代码是否真的绕过了真实依赖,确认隔离生效。