3. Mock的返回值与副作用设置:return_value、side_effect用法、模拟异常抛出

3.1 原理:固定响应 vs. 动态行为

Mock 对象的核心能力在于 行为替代:当被测代码调用某个依赖方法时,开发者可以预先定义该方法的返回结果、执行逻辑甚至异常。这主要通过两个属性实现:

  • return_value —— 固定返回值。无论调用多少次,始终返回同一个对象。适用于状态查询、配置读取等幂等场景。
  • side_effect —— 动态副作用。优先级高于 return_value,可以是可迭代对象(按序返回)、可调用对象(根据入参计算返回)或异常(强制抛出)。适用于时序依赖、错误注入、状态机模拟。

执行流程可归纳为:

  1. 判断 side_effect 是否不为 None。
  2. 若不为 None,则执行 side_effect 逻辑;若为可迭代对象,弹出下一个元素;若为可调用对象,调用并返回结果;若为异常类/实例,直接抛出。
  3. 若 side_effect 为 None,则返回 return_value;若 return_value 也未设置,则返回新的 Mock 对象(默认行为)。
关键原则:side_effect 始终覆盖 return_value。当同时设置两者时,side_effect 优先生效。

3.2 核心实现流程

下表对比了两种机制在典型场景下的差异:

特性return_valueside_effect
目的 固定返回值(幂等) 动态响应、序列依赖、异常模拟
数据类型 任意 Python 对象 Iterable、Callable、Exception 类或实例
优先级 低(仅 side_effect 为 None 时生效) 高(只要不为 None 即覆盖 return_value)
典型场景 隔离 API 调用、配置查询 分页数据、连接超时、状态机
生命周期 无限次有效 可迭代对象耗尽后抛出 StopIteration

标准的实现流程可概括为以下步骤:

  1. 确定模拟目标 —— 明确需要替换的依赖方法(如 requests.get、database.query)。
  2. 选择机制 —— 若只需固定应答,使用 return_value;若需要动态或异常行为,使用 side_effect。
  3. 注入 Mock —— 通过 patch 或直接赋值将 Mock 对象注入被测模块。
  4. 验证结果 —— 执行被测函数,断言返回值和调用次数符合预期。
经验建议:当 side_effect 为可迭代对象时,提前计算好所有期望值并放入列表。避免在测试用例内动态修改迭代器,容易导致测试间耦合。

3.3 代码示例与 API 细节

以下示例基于 unittest.mock(Python 标准库),完整展示了四种核心用法:

3.3.1 固定返回值 —— return_value

from unittest.mock import Mock

# 构造一个返回固定字典的 Mock
def get_user_mock(user_id):
    mock = Mock()
    mock.get_name.return_value = "Alice"
    return mock

mock = get_user_mock(1)
result = mock.get_name()
assert result == "Alice"
print(result)  # Alice

3.3.2 序列返回值 —— side_effect as Iterable

# 模拟分页查询,每次调用返回不同的页
mock_db = Mock()
mock_db.fetch_page.side_effect = [
    ["item1", "item2"],
    ["item3", "item4"],
    []
]

page1 = mock_db.fetch_page()
assert page1 == ["item1", "item2"]

page2 = mock_db.fetch_page()
assert page2 == ["item3", "item4"]

page3 = mock_db.fetch_page()
assert page3 == []

# 第四次调用将抛出 StopIteration
try:
    mock_db.fetch_page()
except StopIteration:
    print("side_effect 序列已耗尽")

3.3.3 动态计算 —— side_effect as Callable

# 根据输入参数动态计算返回值
def calculate_mock(x, y):
    return x * y + 1

mock_calc = Mock()
mock_calc.compute.side_effect = calculate_mock

result = mock_calc.compute(3, 4)
assert result == 13

# 也可以使用 lambda 简化
mock_calc2 = Mock()
mock_calc2.transform.side_effect = lambda s: s.upper()
print(mock_calc2.transform("hello"))  # HELLO

3.3.4 模拟异常 —— side_effect as Exception

# 注入特定的异常,模拟外部服务不可用
mock_service = Mock()
mock_service.get_data.side_effect = ConnectionError("Service timeout")

def fetch_user_data():
    try:
        return mock_service.get_data()
    except ConnectionError as e:
        return f"Fallback due to {e}"

result = fetch_user_data()
assert result == "Fallback due to Service timeout"
print(result)  # Fallback due to Service timeout
常见坑点:当 side_effect 设置为异常类(如 ValueError)而非实例(如 ValueError("msg"))时,Mock 会自动实例化。但若异常类需要特定参数,必须使用实例形式,否则会引发 TypeError。

3.4 调试与验证

在复杂测试场景中,需要通过 Mock 内置属性验证副作用是否正确触发:

  • call_args —— 最近一次调用的参数。
  • call_args_list —— 所有调用的参数列表。
  • call_count —— 调用次数。
  • method_calls —— 子 Mock 的调用记录。
mock = Mock()
mock.side_effect = [10, 20]

mock(1, a=2)
mock(3, b=4)

assert mock.call_count == 2
assert mock.call_args_list == [call(1, a=2), call(3, b=4)]
assert mock.call_args == call(3, b=4)  # 最近一次

若 side_effect 为可调用对象,可在其中添加日志记录:

def trace_side_effect(x):
    print(f"[TRACE] called with {x}")
    return x * 2

mock = Mock(side_effect=trace_side_effect)
result = mock(5)  # 控制台输出 [TRACE] called with 5
assert result == 10

3.5 常见错误与性能考量

错误模式后果解决方案
side_effect 迭代器提前耗尽 调用时抛出 StopIteration 确保列表长度 ≥ 调用次数,或使用无限迭代器(如 itertools.cycle)
混淆 return_value 与 side_effect 优先级 预期固定值但获得动态值 确认 side_effect 是否为 None;使用 return_value 前先置空 side_effect
side_effect 可调用对象修改外部状态 测试间污染,难以定位 尽量使用纯函数;若必须修改状态,在 tearDown 中重置
异常实例未正确初始化 抛出 TypeError 而非目标异常 始终使用异常实例 ValueError("msg"),而非异常类 ValueError

性能优化:

  • 优先使用 return_value 而非 side_effect 可调用对象,后者每次调用都带来额外函数调用开销。
  • 若 side_effect 必须为可调用且被高频调用,考虑将其实现为 C 扩展或缓存计算结果。
  • 使用 Mock(spec=SomeClass) 限制属性访问,避免误写造成额外性能损耗。

3.6 决策流程总结

下图展示了 Mock 在被调用时的完整决策树,帮助快速定位行为设置:

Mock 返回值与副作用决策流程 Mock 调用决策树 调用 Mock 对象 side_effect 为 None? 是 否 执行 side_effect 可迭代:弹出下一项 可调用:调用函数 异常类:直接抛出 return_value 是否设置? 是 否 返回 return_value 返回新 Mock 对象 说明 1. side_effect 不为 None 时,直接执行其逻辑,忽略 return_value。 2. side_effect 为 None 时,判断 return_value;若也未设置,返回自动生成的 Mock 子对象。 3. 可迭代对象耗尽后会抛出 StopIteration,务必确保序列长度足够。
可视化决策逻辑:从上图可以看出,side_effect 是一级分流;仅当其为 None 时,return_value 才有机会生效。日常调试中可以通过打印 mock.side_effect 和 mock.return_value 快速确认当前配置状态。