136

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.

lmiguelvargasf
  • 63,191
  • 45
  • 217
  • 228
charlax
  • 25,125
  • 19
  • 60
  • 71

8 Answers8

244

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()
Jordan
  • 6,083
  • 3
  • 23
  • 30
jamescastlefield
  • 2,464
  • 2
  • 12
  • 2
  • I had to mock a class method decorated as `@property`. This answer worked for me when the other answer (and other answers on many other questions) did not. – AlanSE May 19 '16 at 20:32
  • 3
    this is the way it should be done. I wish there was a way to move the "accepted" answer – vitiral Sep 06 '16 at 19:20
  • 9
    I find including the return value in the context manager call to be slightly cleaner: ``` with mock.patch('MyClass.last_transaction', new_callable=PropertyMock, return_value=Transaction()): ... ``` – wodow Jun 30 '17 at 17:38
  • Indeed, I just moved the accepted answer to this one. – charlax Apr 20 '18 at 07:24
  • 3
    using mock.patch.object is also nice since you don't have to write the class name as a string (not really a problem in the example) and it's easier to detect/fix if you decide to rename a package and haven't updated a test – Kevin Oct 10 '18 at 16:47
  • The thing is, what if I use `mock.patch` as a decorator and on the class? Is there a way? – Eray Erdin Feb 27 '19 at 10:42
  • This replaces the property with a static, non-callable, value. That's not an ideal type of mock. Ideally, it should be replaced by a callable. – Cerin Aug 25 '22 at 22:52
47

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
charlax
  • 25,125
  • 19
  • 60
  • 71
19

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
Tim Swast
  • 14,091
  • 4
  • 38
  • 61
  • 2
    Bam, just what I wanted (autospec'd object with a property). And from a colleague no less ‍♂️ – Mark McDonald Jun 26 '19 at 09:27
  • 1
    Also works well on several objects, because apparently, `Mock` constructor produces a new class everytime: `type(Mock()) == type(Mock())` is `False`. – Codoscope Mar 11 '22 at 14:27
14

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()
lmiguelvargasf
  • 63,191
  • 45
  • 217
  • 228
  • 1
    This also works well with `side_effect=['value1', 'value2', 'value3']` instead of using `return_value`, in case you need several return values consecutively. – zezollo Aug 02 '21 at 12:25
  • that's alot cleaner and mocks in single statement. – A.J. May 19 '22 at 06:49
12

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()
Matthew Schinckel
  • 35,041
  • 6
  • 86
  • 121
Eyal Levin
  • 16,271
  • 6
  • 66
  • 56
3

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
Conchylicultor
  • 4,631
  • 2
  • 37
  • 40
1

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()):
    ...
Simon Charette
  • 5,009
  • 1
  • 25
  • 33
0

I was directed to this question because I wanted to mock the Python version in a test. Not sure whether this is quite relevant to this question, but sys.version is obviously read-only (... though technically an "attribute" rather than a "property", I suppose).

So, after perusing this place and trying some stupidly complicated stuff I realised the answer was simplicity itself:

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

... might help someone.

mike rodent
  • 14,126
  • 11
  • 103
  • 157