Python mock.side_effect:响应列表用完后,为什么不会退回默认返回值

前天 4阅读

测试一个会重试的读取函数,给替身安排四次响应,同时设置默认返回值。原以为第五次会继续返回默认值,测试却出现 StopIteration。这里的响应列表有自己的消费状态;列表耗尽不会自动切换到另一项配置,多调用一次本身可能正是需要发现的回归。

本文在 CPython 3.12.14 执行。把完整代码保存为 demo.py 后运行 python demo.py,只创建内存对象。用明确的字符串和异常作为结果,避免一个自动生成的子 Mock 恰好通过真假判断,让错误调用被隐藏。

把四种响应放进同一条序列

side_effect 设置为可迭代对象后,每次调用消耗下一项。普通元素直接返回,异常元素被抛出,特殊对象 DEFAULT 表示这一轮继续采用正常返回规则。代码把 None 也放进序列,证明它是一个确实返回的空值,与 DEFAULT 的用途不同。

Python mock.side_effect:响应列表用完后,为什么不会退回默认返回值

AI概念示意图:响应卡片依次进入测试入口,空心卡片表示沿用默认规则,队列末端表示序列耗尽。图片不是运行截图。

from unittest.mock import Mock, DEFAULT

fetch = Mock(return_value="fallback", side_effect=[
    "first", DEFAULT, ValueError("bad sample"), None
])
assert fetch("A") == "first"
print("first")
assert fetch("B") == "fallback"
print("fallback")
try:
    fetch("C")
except ValueError as exc:
    assert str(exc) == "bad sample"
    print("planned ValueError")
else:
    raise AssertionError("missing planned error")
assert fetch("D") is None
print("explicit None")

def exhausted(label):
    try:
        fetch(label)
    except StopIteration:
        print(label, "StopIteration")
    else:
        raise AssertionError("exhausted sequence returned a value")

exhausted("fifth")
assert fetch.call_count == 5
fetch.reset_mock()
assert fetch.call_count == 0
exhausted("after reset")
assert fetch.call_count == 1
fetch.side_effect = None
assert fetch("last") == "fallback"
print("disabled: fallback")

异常调用也占用了一次记录

输出前两项分别是 first 和 fallback;第三次抛出预设的 ValueError,第四次返回 None,第五次触发耗尽异常。虽然其中两次没有正常返回,call_count 仍为五。检查重试逻辑时,既要核对最终结果,也要核对调用次数及参数,避免成功值掩盖额外请求。

DEFAULT 必须使用库提供的那个对象,写成同名字符串只会返回字符串本身。这里已显式设置 return_value,所以默认分支结果很清楚。若还配置 wraps 包装真实对象,返回优先级还有下一层,应重新审视是否可能真的调用依赖,不能假定所有替身都完全隔离了外部行为。

清空调用记录不会补回响应

reset_mock 默认清理的是调用记录,没有把已经走到末尾的 side_effect 迭代器倒回起点。代码重置后再次调用仍然抛出 StopIteration,新的调用计数为一。若下一轮测试需要相同响应,应重新赋一个列表或建立新的替身,让每个测试的准备步骤清楚可见。

最后把 side_effect 设为 None,才明确关闭这层响应规则,后续调用返回 fallback。注意这与序列里面放入一个 None 不同:前者改变替身配置,后者只描述某一轮调用的返回值。把两种用法并排验证,能减少调试时随手改值造成的误判。

诊断失败时还可查看 call_args_list,确认耗尽前收到的是哪些参数。调用记录与预设响应是两份状态,清理其中一份不会自动重建另一份。

让序列长度服务于测试意图

如果需求规定最多尝试四次,固定序列耗尽可以暴露第五次调用;不要为让测试变绿而无限补充成功响应。若测试确实需要按输入持续计算结果,可以把 side_effect 设为函数,并在函数中显式定义哪些输入抛异常、哪些返回 DEFAULT,仍要检查被测行为是否符合约定。

本例使用同步 Mock。异步替身 AsyncMock 的耗尽异常是 StopAsyncIteration,不能机械套用这里的捕获类型。也不要在生产重试代码里宽泛吞掉这些测试配置异常;应先判断失败来自业务依赖,还是来自替身数据不足,再修改对应的一层。

参考资料

Python 官方文档:Mock 返回结果的优先级

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。