3. Stub基本概念:定义、工作原理、与Mock的区别、适用条件

Stub(桩)是最简单的测试隔离手段之一。它的核心思想是:用一个轻量级的替身对象替换真实依赖,替身只返回预设好的固定数据,不包含任何验证逻辑。Stub不关心被测代码是否调用了它、调用了多少次,只负责“给数据”——让测试能绕过外部依赖(如数据库、网络服务、文件系统)运行。

3.1 定义与核心特征

  • 定义:Stub是测试替身的一种,它硬编码了内部方法的返回值,使被测单元在隔离环境下获得可控输入。
  • 只出不验:Stub只提供预设响应,不记录调用次数、顺序、参数等交互信息。
  • 状态隔离:Stub通常不维护内部状态(除预设数据外),每次调用返回相同结果。
  • 轻量实现:无需模拟框架,手工类、匿名函数或简单配置即可创建。

3.2 工作原理

Stub的工作遵循“替换→预设→执行→忽略”四步流程:

  1. 替换:在测试准备阶段,用Stub对象替换被测单元的真实依赖(例如:将数据库连接接口替换为返回固定用户的Stub)。
  2. 预设:为Stub的方法分配固定的返回值或异常。
  3. 执行:运行被测代码,代码调用Stub方法,Stub直接返回预设数据,不执行真实依赖的逻辑。
  4. 忽略:测试结束,不需要验证Stub是否被调用;只验证被测单元的输出或状态。
关键点:Stub的“不验证”特性使其最纯粹——它承担的是“数据提供者”角色,而非“行为验证者”。

3.3 Stub 与 Mock 的本质区别

维度 Stub Mock
定义目标 提供预设返回值,隔离外部依赖 验证交互行为(调用次数、参数顺序)
是否验证调用 否 是(核心职责)
返回值性质 固定、硬编码 可动态计算,但仍以验证交互为主
创建复杂度 极低(手工类/匿名函数即可) 较高(通常需要模拟框架)
典型应用场景 读取配置、查询数据库、调用外部API(只关心结果) 验证日志记录、事件推送、缓存写入的次数与顺序

3.4 适用条件

  • 依赖只提供数据:当真实外部系统仅返回数据(如天气API、用户详情查询),且测试不需要验证“如何调用”,只关注被测单元对数据的处理逻辑时,Stub是最佳选择。
  • 依赖运行代价高:如需要启动真实数据库、调用第三方付费接口、或有显著延迟的远程服务,使用Stub可极大提升测试速度。
  • 测试中不需要验证交互:如果测试目的是检验单元的业务逻辑,而非确认它调用了某个依赖多少次,那么Stub比Mock更简洁、不易误判。
  • 向后兼容场景:当依赖接口变化频繁,但测试只关心输入输出转换时,Stub可以快速适配而不影响测试逻辑。
慎用场景:如果测试需要验证“日志是否发送了三次”或“支付消息是否以正确参数推送”,则应使用 Mock 而非 Stub,因为 Stub 不记录调用历史。

3.5 Stub 隔离流程图

Stub 测试隔离流程 测试代码 Stub 对象 预设数据 真实依赖 (未执行) 测试结果校验 调用 get() 返回 隔离阻断 执行完成 说明 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 中临时打印调用日志(不要断言)可以帮助定位被测代码是否真的绕过了真实依赖,确认隔离生效。