Reputation: 64820
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
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
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