Reputation: 26059
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.
Upvotes: 150
Views: 150047
Reputation: 15682
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.
Upvotes: 0
Reputation: 5729
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
Upvotes: 4
Reputation: 69923
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()
Upvotes: 15
Reputation: 18454
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()
Upvotes: 13
Reputation: 14786
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
Upvotes: 20
Reputation: 5116
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()):
...
Upvotes: 1
Reputation: 2654
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()
Upvotes: 263
Reputation: 26059
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
Upvotes: 50