如何使用 mock 模拟只读属性?

如何使用 嘲笑模拟只读属性?

我试过:

setattr(obj.__class__, 'property_to_be_mocked', mock.Mock())

但问题是,它适用于类的所有实例... ... 这破坏了我的测试。

您还有其他想法吗? 我不想模拟完整对象,只想模拟这个特定的属性。

101077 次浏览

实际上,答案是(和往常一样)在 文件中,只是当我按照他们的示例时,我将补丁应用于实例而不是类。

下面是如何做到这一点:

class MyClass:
@property
def last_transaction(self):
# an expensive and complicated DB query here
pass

在测试套件中:

def test():
# Make sure you patch on MyClass, not on a MyClass instance, otherwise
# you'll get an AttributeError, because mock is using settattr and
# last_transaction is a readonly property so there's no setter.
with mock.patch(MyClass, 'last_transaction') as mock_last_transaction:
mock_last_transaction.__get__ = mock.Mock(return_value=Transaction())
myclass = MyClass()
print myclass.last_transaction

我认为更好的方法是将属性模拟为 PropertyMock,而不是直接模拟 __get__方法。

文件中说明,搜索 unittest.mock.PropertyMock: 用作类的属性或其他描述符的模拟。PropertyMock提供了 __get____set__方法,因此可以在获取返回值时指定返回值。

方法如下:

class MyClass:
@property
def last_transaction(self):
# an expensive and complicated DB query here
pass


def test(unittest.TestCase):
with mock.patch('MyClass.last_transaction', new_callable=PropertyMock) as mock_last_transaction:
mock_last_transaction.return_value = Transaction()
myclass = MyClass()
print myclass.last_transaction
mock_last_transaction.assert_called_once_with()

可能是风格问题,但如果你更喜欢测试中的装饰师,@jamescastlefield 的 回答可以改成这样:

class MyClass:
@property
def last_transaction(self):
# an expensive and complicated DB query here
pass


class Test(unittest.TestCase):
@mock.patch('MyClass.last_transaction', new_callable=PropertyMock)
def test(self, mock_last_transaction):
mock_last_transaction.return_value = Transaction()
myclass = MyClass()
print myclass.last_transaction
mock_last_transaction.assert_called_once_with()

如果不想测试是否访问了模拟属性,只需用预期的 return_value对其进行修补。

with mock.patch(MyClass, 'last_transaction', Transaction()):
...

如果要重写其属性的对象是模拟对象,则不必使用 patch

相反,可以创建一个 PropertyMock,然后覆盖模拟的 类型上的属性。例如,重写 mock_rows.pages属性以返回 (mock_page, mock_page,):

mock_page = mock.create_autospec(reader.ReadRowsPage)
# TODO: set up mock_page.
mock_pages = mock.PropertyMock(return_value=(mock_page, mock_page,))
type(mock_rows).pages = mock_pages

如果您使用的是 pytestpytest-mock,那么您可以简化代码并避免使用上下文管理器,即 with语句,如下所示:

def test_name(mocker): # mocker is a fixture included in pytest-mock
mocked_property = mocker.patch(
'MyClass.property_to_be_mocked',
new_callable=mocker.PropertyMock,
return_value='any desired value'
)
o = MyClass()


print(o.property_to_be_mocked) # this will print: any desired value


mocked_property.assert_called_once_with()

如果您需要您的模拟 @property依赖于原来的 __get__,您可以创建您的自定义 MockProperty

class PropertyMock(mock.Mock):


def __get__(self, obj, obj_type=None):
return self(obj, obj_type)

用法:

class A:


@property
def f(self):
return 123




original_get = A.f.__get__


def new_get(self, obj_type=None):
return f'mocked result: {original_get(self, obj_type)}'




with mock.patch('__main__.A.f', new_callable=PropertyMock) as mock_foo:
mock_foo.side_effect = new_get
print(A().f)  # mocked result: 123
print(mock_foo.call_count)  # 1

我之所以被引导到这个问题,是因为我想在测试中模拟 Python 版本。不确定这是否与这个问题相关,但是 sys.version显然是只读的(... 尽管从技术上来说是一个“属性”而不是“属性”,我想)。

因此,在仔细研究了这个地方,并尝试了一些愚蠢复杂的东西之后,我意识到答案其实很简单:

with mock.patch('sys.version', version_tried):
if version_tried == '2.5.2':
with pytest.raises(SystemExit):
import core.__main__
_, err = capsys.readouterr()
assert 'FATAL' in err and 'too old' in err

也许能帮到别人。

模拟对象属性的另一种方法是:

import mock


mocked_object = mock.Mock()
mocked_object.some_property = "Property value"


print(mocked_object.some_property)