Python中如何Mock被装饰类的方法?问题分析与解决
在给带装饰器的Python类应用mock时遇到问题:不带装饰器时,patch能正常生效,但添加装饰器后,同样的patch操作就失效了。需要了解背后原因,以及测试被装饰类的最佳方法。
最小复现代码如下:
模块代码(module.py)
import functools def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @decorator # 注释掉这行,测试就能通过 class Something: def do_external(self): raise Exception("should be mocked") def run(self): self.do_external()
测试代码(test_module.py)
from unittest import TestCase from unittest.mock import Mock, patch from module import Something class TestModule(TestCase): @patch('module.Something.do_external', Mock()) def test_module(self): s = Something() s.run()
当用装饰器装饰类时,@decorator本质是把Something类传入decorator函数,随后module.Something变量会指向装饰器返回的wrapper函数——哪怕用了functools.wraps,它也只是复制原类的元数据,并没有改变wrapper是函数而非类的本质。
而unittest.mock.patch('module.Something.do_external')是尝试给module.Something这个对象的do_external属性打补丁,但此时module.Something已经变成了wrapper函数,不是原来的类了。原类被包装在wrapper内部,patch根本找不到它的do_external方法,自然失效。
注释掉装饰器时,module.Something就是原类,patch能直接修改类的属性,所以可以正常工作。
方法1:修改装饰器,让它返回类而非函数
如果装饰器是你自己实现的,调整逻辑让它返回一个继承原类的子类,而非函数。这样module.Something仍然是类,patch可以正常生效:
import functools def decorator(cls): @functools.wraps(cls, updated=()) # updated=()避免覆盖类的特殊属性 class Wrapper(cls): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) return Wrapper
方法2:通过__wrapped__拿到原类再patch
因为用了functools.wraps,可以通过Something.__wrapped__获取被装饰器包裹的原类,直接patch原类的方法:
class TestModule(TestCase): def test_module(self): original_cls = Something.__wrapped__ with patch.object(original_cls, 'do_external', Mock()): s = Something() s.run()
方法3:直接patch实例的方法
创建实例后,直接替换实例的do_external方法,这种方式不受类是否被装饰的影响:
class TestModule(TestCase): def test_module(self): s = Something() s.do_external = Mock() s.run()
方法4:用patch.object直接patch原类
结合__wrapped__和patch.object,可以写成装饰器形式:
class TestModule(TestCase): @patch.object(Something.__wrapped__, 'do_external', Mock()) def test_module(self): s = Something() s.run()
内容的提问来源于stack exchange,提问作者CodeOcelot

