simonzack
simonzack

Reputation: 20938

python mock property setter while wrapping it

How do you mock a python property setter while wrapping it (i.e. calling the original setter)? The most straightward way is to access __set__, but it's read-only for properties so doesn't work.

from unittest import TestCase
from unittest.mock import patch, PropertyMock


class SomeClass:
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value + 1

    @value.setter
    def value(self, value):
        self._value = value + 1


class TestSomeClass(TestCase):
    def test_value_setter(self):
        instance = SomeClass(0)
        with patch.object(SomeClass.value, '__set__', wraps=SomeClass.value.__set__) as value_setter:
            instance.value = 1
            value_setter.assert_called_with(instance, 1)
        self.assertEquals(instance.value, 3)

There's also the new_callable=PropertyMock in the docs, I've tried to combine it with wrap but haven't gotten it to work yet.

Upvotes: 10

Views: 6410

Answers (2)

simonzack
simonzack

Reputation: 20938

Here's another solution that re-uses PropertyMock, but has the disadvantage of only allowing setters to be mocked in the context.

with patch.object(SomeClass, 'value', new_callable=PropertyMock, wraps=partial(
    SomeClass.value.__set__, instance)
) as value:
    instance.value = 1
    value.assert_called_with(1)
self.assertEquals(instance.value, 3)

or

instance = SomeClass(0)
with patch.object(SomeClass, 'value', PropertyMock(
    side_effect=partial(SomeClass.value.__set__, instance)
)) as value:
    instance.value = 1
    value.assert_called_with(1)
self.assertEquals(instance.value, 3)

Upvotes: 1

Martijn Pieters
Martijn Pieters

Reputation: 1123710

You need to replace the whole property object; you cannot simply replace the setter. this means you'll need to provide a new property object, where the setter is your mock wrapper for the original setter (accessed by the property.fset attribute):

setter_mock = Mock(wraps=SomeClass.value.fset)
mock_property = SomeClass.value.setter(setter_mock)
with patch.object(SomeClass, 'value', mock_property):
    instance.value = 1
    setter_mock.assert_called_with(instance, 1)

I reused the @property.setter decorator here; it returns a new property object with all attributes of the original property except with the setter replaced by whatever was passed in; see How does the @property decorator work?

Upvotes: 6

Related Questions