如何用mock模拟readonly属性?

时间:2012-08-06 21:45:09

标签: python unit-testing mocking

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

我试过了:

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

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

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

7 个答案:

答案 0 :(得分:121)

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

答案 1 :(得分:39)

实际上,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

答案 2 :(得分:3)

可能是风格问题,但如果你喜欢测试中的装饰者,@ jamescastlefield' s 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(mock_last_transaction):
        mock_last_transaction.return_value = Transaction()
        myclass = MyClass()
        print myclass.last_transaction
        mock_last_transaction.assert_called_once_with()

答案 3 :(得分:3)

如果您将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()

答案 4 :(得分:1)

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

答案 5 :(得分:0)

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

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

答案 6 :(得分:0)

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