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


92

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

我试过了:

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

但问题在于它随后适用于该类的所有实例...这破坏了我的测试。

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

Answers:


164

我认为更好的方法是将该属性模拟为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()

我不得不模拟一个装饰为的类方法@property。当其他答案(以及许多其他问题的其他答案)不起作用时,此答案对我有用。
AlanSE '16

2
这是应该做的方式。我希望有一种方法可以转移“已接受”的答案
病毒性的

4
我发现在上下文管理器调用中包含返回值会稍微更干净:```与ockock.patch('MyClass.last_transaction',new_callable = PropertyMock,return_value = Transaction()):...```
wodow

确实,我只是将接受的答案移到了这个答案上。
charlax

1
使用mock.patch.object也是不错的选择,因为您不必将类名写为字符串(在示例中并不是真正的问题),并且如果您决定重命名软件包而不必重新命名软件包,则更容易检测/修复。更新了测试
凯文

41

实际上,答案是(与往常一样)在文档中,只是我在遵循示例时将补丁应用到实例而不是类。

这是操作方法:

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

14
人们应该使用另一个例子。mock.PropertyMock是这样做的方法!
病毒性的2016年

4
没错,在撰写本文PropertyMock时还不存在。
夏洛克斯

6

如果您要覆盖其属性的对象是模拟对象,则不必使用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

1
Bam,正是我想要的(带有属性的autospec'd对象)。而来自同事的less️
Mark McDonald

6

可能是样式问题,但是如果您在测试中更喜欢装饰器,@ 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()

6

如果您将pytest与一起使用pytest-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()

0

如果您不想测试是否访问了嘲笑的属性,则可以简单地用期望的对其进行修补return_value

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

0

如果您需要嘲笑@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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.