如何让unittest.mock._patch实例支持下标访问与迭代?
问题与解决方案:unittest.mock.patch模拟可迭代/可下标访问类的问题
问题背景
我有一个实现了下标访问与迭代功能的Foo类:
from typing import List class Foo: def __init__(self, iterable: List[...]): self.iterable = iterable def __getitem__(self, i: int): return self.iterable[i] def __iter__(self): return iter(self.iterable)
该类的实例支持直接下标访问和迭代:
>>> foo = Foo(iterable=['a', 'b', 'c']) >>> foo[-1] 'c' >>> for x in foo: ... print(x) ... a b c
我尝试用unittest.mock.patch测试该类,代码如下:
from unittest.mock import patch def test_foo(): mock_foo = patch('my_module.Foo', autospec=True) mock_foo.iterable = ['a', 'b', 'c'] mock_foo.__getitem__ = lambda self, i: self.iterable[i] mock_foo.__iter__ = lambda self: iter(self.iterable)
但直接对mock_foo进行下标访问或迭代时失败:
>>> mock_foo[-1] TypeError: '_patch' object is not subscriptable >>> list(mock_foo) TypeError: '_patch' object is not iterable
奇怪的是,直接调用魔法方法却能正常工作:
>>> mock_foo.__getitem__(mock_foo, -1) 'c' >>> list(mock_foo.__iter__(mock_foo)) ['a', 'b', 'c']
原因分析
出现这种不一致的核心原因是:
patch('my_module.Foo')返回的**_patch对象是一个补丁管理器**,它的作用是临时替换目标类,而非模拟的Foo类实例或类本身。- Python的特殊魔法方法(如
__getitem__、__iter__)调用逻辑是:当使用obj[i]或iter(obj)时,Python会在obj的类层面查找对应的方法。直接给_patch对象赋值__getitem__,并不会让Python将其识别为支持下标访问的类型——因为_patch类本身没有实现这些方法。 - 直接调用
mock_foo.__getitem__(mock_foo, -1)能工作,只是因为手动调用了赋值的函数,但这不符合Python对特殊方法的触发逻辑。
解决方法与更优实践
方法1:通过上下文管理器/装饰器获取模拟类,配置实例行为
正确的做法是使用patch的上下文管理器或装饰器,获取被替换的模拟类对象,然后配置其return_value(即模拟类实例的行为):
from unittest.mock import patch def test_foo(): with patch('my_module.Foo', autospec=True) as mock_foo_class: # 获取模拟类的实例对象(调用Foo()会返回的对象) mock_instance = mock_foo_class.return_value # 配置实例的属性和魔法方法 mock_instance.iterable = ['a', 'b', 'c'] mock_instance.__getitem__.side_effect = lambda i: mock_instance.iterable[i] mock_instance.__iter__.return_value = iter(mock_instance.iterable) # 正常使用模拟实例 assert mock_instance[-1] == 'c' assert list(mock_instance) == ['a', 'b', 'c']
方法2:直接使用真实实例作为补丁返回值(最简单)
如果Foo类的实例化没有副作用,直接用真实的Foo实例作为patch的return_value是最简洁的方式,完全复现原类行为:
from unittest.mock import patch from my_module import Foo def test_foo(): real_foo = Foo(['a', 'b', 'c']) with patch('my_module.Foo', return_value=real_foo): # 调用my_module.Foo()会返回真实实例 foo = Foo() assert foo[-1] == 'c' assert list(foo) == ['a', 'b', 'c']
方法3:用MagicMock手动模拟实例
如果需要更灵活的控制(比如断言方法调用次数),可以用MagicMock创建模拟实例,再让patch指向它:
from unittest.mock import patch, MagicMock def test_foo(): mock_instance = MagicMock() mock_instance.iterable = ['a', 'b', 'c'] # 配置下标访问逻辑 mock_instance.__getitem__.side_effect = lambda i: mock_instance.iterable[i] # 配置迭代逻辑 mock_instance.__iter__.return_value = iter(mock_instance.iterable) with patch('my_module.Foo', return_value=mock_instance): from my_module import Foo foo = Foo() assert foo[-1] == 'c' assert list(foo) == ['a', 'b', 'c'] # 断言方法调用次数 mock_instance.__getitem__.assert_called_once_with(-1)
内容的提问来源于stack exchange,提问作者themagicalkamja
相关产品推荐
相关产品推荐

