Как имитировать свойство только для чтения с помощью mock?

92

Как вы имитируете свойство только для чтения с помощью mock ?

Я старался:

setattr(obj.__class__, 'property_to_be_mocked', mock.Mock())

но проблема в том, что затем он применяется ко всем экземплярам класса ... что нарушает мои тесты.

Есть ли у вас другие идеи? Я не хочу высмеивать весь объект, только это конкретное свойство.

Charlax
источник

Ответы:

167

Я думаю, что лучший способ - издеваться над свойством as PropertyMock, а не __get__напрямую издеваться над методом.

В документации указано , что ищите unittest.mock.PropertyMock: Мок, предназначенный для использования в качестве свойства или другого дескриптора в классе. PropertyMockпредоставляет __get__и __set__методы, чтобы вы могли указать возвращаемое значение при его получении.

Вот как:

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()
Jamescastlefield
источник
Мне пришлось издеваться над методом класса, оформленным как @property. Этот ответ сработал для меня, когда другой ответ (и другие ответы на многие другие вопросы) не помог.
AlanSE
3
так и должно быть. Я бы хотел, чтобы был способ переместить «принятый» ответ
vitiral
4
Я считаю, что включение возвращаемого значения в вызов диспетчера контекста немного чище: `` с mock.patch ('MyClass.last_transaction', new_callable = PropertyMock, return_value = Transaction ()): ... ''
wodow
Действительно, я просто переместил принятый ответ на этот.
charlax
1
использование mock.patch.object также хорошо, поскольку вам не нужно записывать имя класса в виде строки (на самом деле это не проблема в примере), и его легче обнаружить / исправить, если вы решили переименовать пакет и не обновил тест
Kevin
41

Собственно, ответ был (как обычно) в документации , просто я применял патч к экземпляру, а не к классу, когда следовал их примеру.

Вот как это сделать:

class MyClass:
    @property
    def last_transaction(self):
        # an expensive and complicated DB query here
        pass

В наборе тестов:

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
источник
14
люди должны использовать другой пример. mock.PropertyMockспособ сделать это!
vitiral
4
Правильно, на момент написания PropertyMockне существовало.
charlax
6

Если объект, свойство которого вы хотите переопределить, является фиктивным объектом, вам не нужно использовать patch.

Вместо этого можно создать, PropertyMockа затем переопределить свойство для типа макета. Например, чтобы переопределить mock_rows.pagesвозвращаемое свойство (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
Тим Сваст
источник
1
Бам, именно то, что я хотел (объект autospec'd со свойством). И от коллеги не меньше 🙋‍♂️
Марк Макдональд
6

Вероятно, дело в стиле, но если вы предпочитаете декораторы в тестах, ответ @ jamescastlefield можно изменить на что-то вроде этого:

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()
Эяль Левин
источник
6

В случае, если вы используете pytestвместе pytest-mock, вы можете упростить свой код, а также избежать использования диспетчера контекста, т. Е. Следующего withоператора:

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
источник
0

Если вы не хотите проверять, был ли получен доступ к фиктивному свойству, вы можете просто исправить его с помощью ожидаемого return_value.

with mock.patch(MyClass, 'last_transaction', Transaction()):
    ...
Саймон Шаретт
источник
0

Если вам нужно, чтобы ваш макет @propertyполагался на оригинал __get__, вы можете создать свой собственныйMockProperty

class PropertyMock(mock.Mock):

    def __get__(self, obj, obj_type=None):
        return self(obj, obj_type)

Применение:

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
Конхиликулятор
источник