100
votes

How do you mock a readonly property with mock?

I tried:

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

but the issue is that it then applies to all instances of the class... which breaks my tests.

Do you have any other idea? I don't want to mock the full object, only this specific property.

7

7 Answers

192
votes

I think the better way is to mock the property as PropertyMock, rather than to mock the __get__ method directly.

It is stated in the documentation, search for unittest.mock.PropertyMock: A mock intended to be used as a property, or other descriptor, on a class. PropertyMock provides __get__ and __set__ methods so you can specify a return value when it is fetched.

Here is how:

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

Actually, the answer was (as usual) in the documentation, it's just that I was applying the patch to the instance instead of the class when I followed their example.

Here is how to do it:

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

In the test suite:

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
10
votes

Probably a matter of style but in case you prefer decorators in tests, @jamescastlefield's answer could be changed to something like this:

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

If the object whose property you want to override is a mock object, you don't have to use patch.

Instead, can create a PropertyMock and then override the property on the type of the mock. For example, to override mock_rows.pages property to return (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
6
votes

In case you are using pytest along with pytest-mock, you can simplify your code and also avoid using the context manger, i.e., the with statement as follows:

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

If you need your mocked @property to rely on the original __get__, you can create your custom MockProperty

class PropertyMock(mock.Mock):

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

Usage:

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
1
votes

If you don't want to test whether or not the mocked property was accessed you can simply patch it with the expected return_value.

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