Cerin
Cerin

Reputation: 64820

How to make a mocked class property call a function for each access?

How do you mock a class property so that accessing the property calls an alternate method or function in Python?

My question is very similar to this one but all of those solutions result in a mocked property that returns a static unchanging value.

I want to do something like:

import unittest
from unittest import mock
from unittest.mock import PropertyMock

from faker import Faker


class MyClass:

    @property
    def text(self):
        return 'blargh'

        
class Tests(unittest.TestCase):

    @mock.patch('__main__.MyClass.text', new_callable=PropertyMock)
    def test_mock_error(self, mock_text):        
        #mock_text.return_value = Faker().text() # outputs the same text
        mock_text.return_value = lambda: Faker().text() # outputs a function
        print('a:', MyClass().text)
        print('b:', MyClass().text)


if __name__ == '__main__':
    unittest.main()

and have a: and b: output different text. But currently the mocked property doesn't call the lambda function is mock it to.

I can get it to work if I use a callback proxy like:

@mock.patch('__main__.MyClass.text', new_callable=PropertyMock)
def test_mock_error(self, mock_text):        
    from proxytypes3 import CallbackProxy
    mock_text.return_value = CallbackProxy(lambda: Faker().text())
    print('a:', MyClass().text)
    print('b:', MyClass().text)

Is there some way to accomplish this using the vanilla mock module?

Upvotes: 0

Views: 571

Answers (2)

Kache
Kache

Reputation: 16727

If you only want to return some canned list of responses, @chepner's answer will work, i.e. assign some iterable to the mock's side_effect. It can also be an infinite iterable as well.

mock = Mock(side_effect=itertools.count())
mock()  # => 0
mock()  # => 1
mock()  # => 2

But if you want it to actually call a function, then just assign that side_effect to a callable.

mock = Mock(side_effect=lambda: datetime.datetime.now())

(mock() - datetime.datetime.now()).total_seconds() < 0.0001  # True
(mock() - datetime.datetime.now()).total_seconds() < 0.0001  # True

And for completion, to do this with a PropertyMock let's adapt this example from documentation

>>> m = MagicMock()
>>> p = PropertyMock(side_effect=itertools.count())
>>> type(m).foo = p
>>> m.foo
0
>>> m.foo
1

For non-Mock classes like your MyClass, you'll want to patch PropertyMock instead of setattr, b/c Mocks are a little special: https://github.com/python/cpython/blob/fa118f0cd32e9b6cba68df10a176b502407243c8/Lib/unittest/mock.py#L405-L407

Putting it all together, running:

import unittest
from unittest.mock import PropertyMock, patch


class MyClass:
    @property
    def text(self):
        return 'blargh'


state = ['a', 'b', 'c']


def stateful_func():
    return state.pop()


class Tests(unittest.TestCase):

    @patch('__main__.MyClass.text', new=PropertyMock(side_effect=stateful_func))
    def test_mock_error(self):
        print('a:', MyClass().text)
        print('b:', MyClass().text)


if __name__ == '__main__':
    unittest.main()

prints:

a: c
b: b
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Upvotes: 1

chepner
chepner

Reputation: 531948

Use side_effect to provide a list of values to cycle through.

mock_test.side_effect = [Faker().text(), Faker.text()]

Your attempt never calls the function; you explicitly said that MyClass().text should resolve to a function, not the value the function would return.

Upvotes: 0

Related Questions