pytest 默认断言失败时会调用 repr() 渲染对象,而循环引用会导致无限递归或 RecursionError;解决方案包括:用 deepdiff 替代 ==、临时 patch __repr__、或编写语义级断言函数并配合 --tb=short。

为什么 pytest 默认会卡在循环引用对象的断言里?
因为 pytest 在失败时自动调用 repr() 或 pprint() 渲染预期值与实际值,而含循环引用的对象(比如 A 持有 B,B 又持有 A)会让 repr 无限递归展开,最终触发 RecursionError: maximum recursion depth exceeded 或直接卡死。
这不是你代码逻辑错,是 pytest 的断言辅助机制“太尽责”了。
- 典型触发场景:
assert obj1 == obj2,其中obj1和obj2是手动构造的带父子双向引用的模型实例(如 SQLAlchemy 模型、自定义树节点) - 哪怕你重写了
__eq__,只要 pytest 尝试展示差异,就可能掉进repr坑里 -
pytest -s也救不了——输出被阻塞在内部格式化阶段,根本到不了你的print
用 deepdiff 替代原生 == 断言
deepdiff 默认跳过循环引用检测,且支持显式控制遍历深度和忽略字段,是目前最轻量、最可控的替代方案。
- 安装:
pip install deepdiff - 基本用法:
from deepdiff import DeepDiff; assert not DeepDiff(a, b, ignore_order=True, report_repetition=True) - 关键参数:
-
ignore_order=True:避免因 list 顺序不同误报(常见于 children 列表) -
max_diff_depth=5:防深层嵌套失控(默认无限制) -
exclude_paths=["root._parent", "root._children"]:直接过滤掉已知循环字段
-
- 注意:
DeepDiff返回 dict,空 dict 表示相等,所以必须写assert not DeepDiff(...),而不是assert DeepDiff(...) == {}(后者在 diff 很大时可能慢且不直观)
给测试对象加临时 __repr__ 防护
如果你无法引入第三方库,或只想快速让现有测试跑通,可以在测试 setup 阶段为对象打一个“安全 repr 补丁”,不改生产代码,只影响当前测试上下文。
- 做法:用
unittest.mock.patch临时替换类的__repr__ - 示例:
from unittest.mock import patch <p>def test_tree_with_cycle(): root = TreeNode("root") child = TreeNode("child") root.add_child(child) child.parent = root # 形成循环</p><pre class="brush:php;toolbar:false;">with patch.object(TreeNode, "__repr__", lambda self: f"<treenode>"): assert root.children == [child] # 此时 assert 不再崩溃</treenode> - ⚠️ 注意:patch 要作用在类上,不是实例;且仅对当前测试块生效,不影响其他测试
- 不推荐长期使用——它掩盖了真实结构,不利于调试;但作为 CI 临时兜底很有效
用 pytest --tb=short + 自定义断言函数规避渲染
当对象结构极其复杂、连 deepdiff 都难以稳定比对时,退回到语义级断言更可靠:只验证你真正关心的字段路径是否一致,绕过全量结构比较。
- 写一个极简校验函数,例如:
def assert_node_equal(actual, expected): assert actual.name == expected.name assert len(actual.children) == len(expected.children) for a, e in zip(actual.children, expected.children): assert_node_equal(a, e) # 递归但可控,不依赖 repr - 运行时加
--tb=short或--tb=line,避免 pytest 尝试展开整个对象做 traceback 渲染 - 这种写法本质是“契约式断言”:你明确声明哪些字段重要,哪些可以忽略(比如内存地址、缓存属性、循环引用指针),反而更贴近测试意图
循环引用本身不是 bug,但测试时暴露它的方式往往比问题本身更让人头疼。真正麻烦的从来不是怎么比对,而是比对失败后你根本看不到错误在哪——所以优先堵住 repr 泄漏,再谈逻辑验证。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











