递归函数测试失败主因常是栈溢出、边界漏判或深度失控;pytest 默认不捕获RecursionError,因其在函数体执行前由解释器截断,导致进程崩溃而非抛出可捕获异常。

递归函数测试失败,八成不是逻辑错,而是没测到栈溢出、边界漏判或深度失控——pytest 默认不捕获 RecursionError,它直接让测试用例崩溃退出,连断言失败都看不到。
为什么 pytest.raises(RecursionError) 常常不生效
Python 的 RecursionError 是在解释器检测到调用栈超限时抛出的,它发生在函数真正执行到“第 N+1 层”之前,属于底层机制级异常。而 pytest.raises() 只能捕获被 Python 正常 raise 出来的异常;如果递归在进入函数体前就因栈满被截断,pytest 根本收不到异常对象,测试进程直接 abort。
实操建议:
- 不要依赖
pytest.raises(RecursionError)来验证“过深会报错”,它不可靠 - 改用
sys.setrecursionlimit()主动压低限制(例如设为 50),再配合@pytest.mark.timeout(0.05)防卡死 - 对关键递归函数,在入口加显式深度计数参数(如
_depth=0),并在内部用assert _depth 做软拦截
如何用 len(inspect.stack()) 实时验证实际调用层数
光看输入参数推算“应该递归多少层”不保险——分支逻辑、提前 return、异常跳转都会让实际栈深和预期不一致。用 inspect.stack() 能拿到当前真实调用栈长度,是唯一可信的深度观测手段。
立即学习“Python免费学习笔记(深入)”;
实操建议:
- 在递归函数最开头插入:
assert len(inspect.stack()) (+10 是预留解释器开销) - 测试时传入已知深度的输入(如树高为 3 的二叉树),断言栈长落在
[base_depth + 3, base_depth + 5]区间内 - 避免在生产代码里长期保留
inspect.stack()——它极慢,仅用于测试钩子
mock 递归调用本身来隔离单层逻辑
当你要验证“第 2 层返回值是否正确影响了第 1 层的计算”,又不想跑完整递归链时,mock 递归函数自身比 mock 外部依赖更干净。但必须 patch 到「被测模块中 import 的位置」,而不是函数定义处。
实操建议:
- 假设被测函数在
mylib.tree.py中写为from mylib.utils import traverse,则 patch 目标是mylib.tree.traverse,不是mylib.utils.traverse - 用
side_effect控制返回序列:mock_traverse.side_effect = [42, 17, ValueError("stop here")] - 测试后务必检查:
mock_traverse.call_count == 2,防止 patch 失败导致真实递归仍被执行
递归深度测试必须覆盖临界点,而非均匀采样
只测 [1, 10, 100] 深度毫无意义——Python 默认递归限制通常是 1000,你永远不知道函数在 999 层时是否开始变慢,或在 1001 层是否静默卡死(某些 C 扩展可能不触发 RecursionError)。
实操建议:
- 深度参数化列表应为:
[1, 5, 998, 999, 1000, 1001] - 对每个深度,加
@pytest.mark.timeout(0.1)(需装pytest-timeout) - 记录并比对耗时曲线:若从 999→1000 耗时突增 10 倍,说明接近栈压力拐点,需预警
递归测试最难的不是写用例,而是意识到:你测的从来不是“函数是否返回正确值”,而是“它是否在正确的深度、以正确的路径、用正确的资源消耗抵达那个值”。栈深、调用次数、异常类型、耗时拐点——这些才是递归函数真正的契约。


















