如何Mock attr.ib()的默认值以避免单元测试抛出异常?
init=False 属性默认值的 Mock 问题 这个问题我之前也踩过坑,核心原因很明确:你定义_prop时用了default=OtherClass(),这行代码是在Python加载ExampleClass类的阶段就执行了——也就是测试运行前,真实的OtherClass实例已经被创建并作为默认值绑定到属性上了。而你测试里的mocker.patch是在测试运行时才生效的,自然没法影响这个早早就存在的真实实例。
下面给你两个可行的解决方案,优先推荐第一个:
方案1:用工厂函数延迟实例化(最稳妥)
attrs提供了factory参数来替代直接赋值的默认值,它会在每次创建ExampleClass实例的时候才调用传入的函数/类来生成默认值。修改你的ExampleClass定义:
@attrs(auto_attribs=True) class ExampleClass: # 用 factory 替代直接的 OtherClass(),延迟实例化时机 _prop: OtherClass = attrib(init=False, factory=OtherClass)
这样调整后,当你在测试里patchOtherClass之后,创建sut = ExampleClass()时,factory=OtherClass会调用被patch后的Mock类,生成一个MagicMock实例赋值给self._prop。这时候调用self._prop.some_func()就会触发Mock的方法,不会再抛出NotImplementedException了。
方案2:在patch后覆盖类的默认值(应急方案,不推荐)
如果因为某些原因没法修改原类的代码,那可以在测试里patch之后,手动覆盖ExampleClass中_prop属性的默认值。attrs会把类的属性元信息存在__attrs_attrs__这个类属性里,我们可以找到对应属性修改它的默认值:
def test_example(mocker): mock_other_cls = mocker.patch("path.to.example_class.OtherClass") # 找到 _prop 属性的元信息,替换默认值为Mock实例 for attr in ExampleClass.__attrs_attrs__: if attr.name == "_prop": attr.default = mock_other_cls() break sut = ExampleClass() sut.some_func()
这个方法能解决问题,但缺点是依赖attrs的内部实现(__attrs_attrs__),如果后续attrs版本更新改变了这个结构,测试就会失效,所以只适合应急用。
再啰嗦一句为什么直接patch没用
再明确一下时间线:
- Python加载
ExampleClass类 → 执行OtherClass()创建真实实例 → 把这个实例作为_prop的默认值存在类里 - 测试运行 → 执行
mocker.patch替换OtherClass为Mock - 创建
ExampleClass实例 → 用之前已经存在的真实实例作为_prop的默认值
所以patch根本碰不到这个早早就创建好的真实实例,自然会抛异常。而用factory的话,步骤3才会调用OtherClass(),这时候已经是Mock类了,生成的就是Mock实例。
内容的提问来源于stack exchange,提问作者Cameron Hurst

