使用PropertyMock的side_effect时能否访问self对象?
问题:Mock类的Property属性时无法访问self对象
尝试Mock类的property属性,希望根据对象其他状态控制返回值,编写测试代码后运行报错:TypeError: ... missing 1 required positional argument: 'self'
示例代码
import datetime from pytest_mock import MockFixture class MyObject: def __init__(self, name: str): self._name = name self._creation_time = datetime.datetime.now() @property def creation_time(self) -> datetime.datetime: # 一系列计算逻辑 # return_value = ... return return_value def test_ordered_creation_time(mocker: MockFixture) -> None: def creation_time_side_effect(self) -> datetime.datetime: if self._name == 'first_created': return datetime.datetime(2020, 1, 1) elif self._name == 'second_created': return datetime.datetime(2020, 2, 1) mock_creation_time = mocker.patch.object( MyObject, 'creation_time', new_callable=mocker.PropertyMock) mock_creation_time.side_effect = creation_time_side_effect first_created = MyObject('first_created') second_created = MyObject('second_created') assert first_created.creation_time < second_created.creation_time
核心报错
E TypeError: test_ordered_creation_time.<locals>.creation_time_side_effect() missing 1 required positional argument: 'self'
完整报错栈:
====================================================================================================================================================================================== FAILURES ====================================================================================================================================================================================== _____________________________________________________________________________________________________________________________________________________________________________ test_ordered_creation_time _____________________________________________________________________________________________________________________________________________________________________________ mocker = <pytest_mock.plugin.MockerFixture object at 0x103fb1210> def test_ordered_creation_time(mocker: MockFixture) -> None: def creation_time_side_effect(self) -> datetime.datetime: if self._name == 'first_created': return datetime.datetime(2020, 1, 1) elif self._name == 'second_created': return datetime.datetime(2020, 2, 1) mock_creation_time = mocker.patch.object( MyObject, 'creation_time', new_callable=mocker.PropertyMock) mock_creation_time.side_effect = creation_time_side_effect first_created = MyObject('first_created') second_created = MyObject('second_created') > assert first_created.creation_time < second_created.creation_time dummy.py:38: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ /opt/homebrew/Cellar/python@3.11/3.11.4/Frameworks/Python.framework/Versions/3.11/lib/python3.11/unittest/mock.py:2946: in __get__ return self() /opt/homebrew/Cellar/python@3.11/3.11.4/Frameworks/Python.framework/Versions/3.11/lib/python3.11/unittest/mock.py:1124: in __call__ return self._mock_call(*args, **kwargs) /opt/homebrew/Cellar/python@3.11/3.11.4/Frameworks/Python.framework/Versions/3.11/lib/python3.11/unittest/mock.py:1128: in _mock_call return self._execute_mock_call(*args, **kwargs) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <PropertyMock name='creation_time' id='4361753680'>, args = (), kwargs = {}, effect = <function test_ordered_creation_time.<locals>.creation_time_side_effect at 0x103f9e3e0> def _execute_mock_call(self, /, *args, **kwargs): # separate from _increment_mock_call so that awaited functions are # executed separately from their call, also AsyncMock overrides this method effect = self.side_effect if effect is not None: if _is_exception(effect): raise effect elif not _callable(effect): result = next(effect) if _is_exception(result): raise result else: > result = effect(*args, **kwargs) E TypeError: test_ordered_creation_time.<locals>.creation_time_side_effect() missing 1 required positional argument: 'self' /opt/homebrew/Cellar/python@3.11/3.11.4/Frameworks/Python.framework/Versions/3.11/lib/python3.11/unittest/mock.py:1189: TypeError
原因分析
是的,PropertyMock的side_effect确实无法直接接收self参数,原因如下:
PropertyMock模拟的是属性的getter方法,但它的side_effect机制会把传入的函数当作普通函数调用,不会自动绑定到实例对象上。- 从报错栈可见,调用副作用函数时
args是空元组,没有传入实例对象,导致自定义的副作用函数因缺少self参数报错。
解决方法
方法1:自定义PropertyMock子类,传递实例对象
重写__get__方法,在调用side_effect时主动传入实例对象:
import datetime from pytest_mock import MockFixture from unittest.mock import PropertyMock class InstanceBoundPropertyMock(PropertyMock): def __get__(self, instance, owner): if self.side_effect is not None: return self.side_effect(instance) return super().__get__(instance, owner) class MyObject: def __init__(self, name: str): self._name = name self._creation_time = datetime.datetime.now() @property def creation_time(self) -> datetime.datetime: return self._creation_time def test_ordered_creation_time(mocker: MockFixture) -> None: def creation_time_side_effect(instance) -> datetime.datetime: if instance._name == 'first_created': return datetime.datetime(2020, 1, 1) elif instance._name == 'second_created': return datetime.datetime(2020, 2, 1) mock_creation_time = mocker.patch.object( MyObject, 'creation_time', new_callable=InstanceBoundPropertyMock) mock_creation_time.side_effect = creation_time_side_effect first_created = MyObject('first_created') second_created = MyObject('second_created') assert first_created.creation_time < second_created.creation_time
方法2:针对单个实例单独Mock属性
如果不需要全局Mock类的属性,可直接为每个实例单独设置Mock值:
def test_ordered_creation_time(mocker: MockFixture) -> None: first_created = MyObject('first_created') second_created = MyObject('second_created') # 分别替换每个实例的creation_time属性 mocker.patch.object(first_created, 'creation_time', new_callable=PropertyMock, return_value=datetime.datetime(2020,1,1)) mocker.patch.object(second_created, 'creation_time', new_callable=PropertyMock, return_value=datetime.datetime(2020,2,1)) assert first_created.creation_time < second_created.creation_time
方法3:钩子原生属性的__get__方法
利用属性描述符的__get__机制,直接替换原生getter逻辑:
def test_ordered_creation_time(mocker: MockFixture) -> None: original_get = MyObject.creation_time.__get__ def mock_get(instance, owner): if instance._name == 'first_created': return datetime.datetime(2020,1,1) elif instance._name == 'second_created': return datetime.datetime(2020,2,1) return original_get(instance, owner) mocker.patch.object(MyObject.creation_time, '__get__', mock_get) first_created = MyObject('first_created') second_created = MyObject('second_created') assert first_created.creation_time < second_created.creation_time
内容的提问来源于stack exchange,提问作者Hakan Baba
相关产品推荐
相关产品推荐

