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

Posted

技术标签:

【中文标题】如何用模拟模拟只读属性?【英文标题】:How to mock a readonly property with mock? 【发布时间】:2012-08-03 21:43:51 【问题描述】:

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

我试过了:

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

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

你还有什么想法吗?我不想模拟整个对象,只模拟这个特定的属性。

【问题讨论】:

【参考方案1】:

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

在documentation中有说明,搜索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 的类方法。当另一个答案(以及许多其他问题的其他答案)不起作用时,这个答案对我有用。 这是应该做的。我希望有办法移动“已接受”的答案 我发现在上下文管理器调用中包含返回值会稍微干净一些:``` with mock.patch('MyClass.last_transaction', new_callable=PropertyMock, return_value=Transaction()): 。 .. ``` 确实,我只是将接受的答案移到了这个。 使用 mock.patch.object 也很好,因为您不必将类名写为字符串(在示例中不是真正的问题),如果您决定更容易检测/修复重命名一个包并且没有更新一个测试【参考方案2】:

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

这是怎么做的:

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

【讨论】:

人们应该使用另一个例子。 mock.PropertyMock 是这样做的方法! 没错,在撰写本文时PropertyMock 不存在。【参考方案3】:

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

相反,可以创建一个PropertyMock,然后覆盖模拟的type 上的属性。比如覆盖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

【讨论】:

Bam,正是我想要的(具有属性的自动规范对象)。并且来自一位同事?‍♂️【参考方案4】:

可能是风格问题,但如果您更喜欢测试中的装饰器,@jamescastlefield 的 answer 可以更改为这样的:

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()

【讨论】:

【参考方案5】:

如果您同时使用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()

【讨论】:

这也适用于side_effect=['value1', 'value2', 'value3'],而不是使用return_value,以防您需要连续的多个返回值。【参考方案6】:

如果您需要模拟的@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

【讨论】:

【参考方案7】:

如果您不想测试是否访问了模拟属性,您可以简单地使用预期的return_value 对其进行修补。

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

【讨论】:

【参考方案8】:

我被引导到这个问题是因为我想在测试中模拟 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

...可能会帮助某人。

【讨论】:

以上是关于如何用模拟模拟只读属性?的主要内容,如果未能解决你的问题,请参考以下文章

使用 moq 模拟虚拟只读属性

Objective C 类扩展 - 如何用 readwrite 方法覆盖只读?

HTML 表单只读 SELECT 标记/输入

HTML 表单只读 SELECT 标记/输入

如何用 phpunit 模拟 Laravel Eloquent 模型?

关于C# PropertyGrid控件的属性只读