Jak mockować właściwość tylko do odczytu za pomocą makiety?

92

Jak wyśmiewać właściwość tylko do odczytu za pomocą makiety ?

Próbowałem:

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

ale problem polega na tym, że dotyczy to wszystkich instancji klasy ... co psuje moje testy.

Masz inny pomysł? Nie chcę kpić z całego obiektu, tylko z tej konkretnej właściwości.

charlax
źródło

Odpowiedzi:

167

Myślę, że lepszym sposobem jest wyśmiewanie właściwości jako PropertyMock, a nie __get__bezpośrednie kpienie z metody.

Stwierdza się w dokumentacji , szukaj unittest.mock.PropertyMock: pozorowanej przeznaczone do stosowania jako własność lub innego deskryptora, w klasie. PropertyMockzapewnia __get__i __set__metody, dzięki czemu można określić wartość zwracaną podczas pobierania.

Oto jak:

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
źródło
Musiałem kpić z metody klasowej odznaczonej jako @property. Ta odpowiedź zadziałała dla mnie, gdy inna odpowiedź (i inne odpowiedzi na wiele innych pytań) nie zadziałała.
AlanSE
3
tak to powinno być zrobione. Żałuję, że nie ma sposobu na przeniesienie „zaakceptowanej” odpowiedzi
witiralny
4
Uważam, że włączenie wartości zwracanej w wywołaniu menedżera kontekstu jest nieco czystsze: `` z mock.patch ('MyClass.last_transaction', new_callable = PropertyMock, return_value = Transaction ()): ... ``
wodow
Rzeczywiście, właśnie przeniosłem zaakceptowaną odpowiedź na tę.
charlax
1
używanie mock.patch.object jest również przyjemne, ponieważ nie musisz pisać nazwy klasy jako łańcucha (nie jest to naprawdę problem w przykładzie) i łatwiej jest wykryć / naprawić, jeśli zdecydujesz się zmienić nazwę pakietu, a tego nie zrobiłeś zaktualizował test
Kevin
41

Właściwie odpowiedź brzmiała (jak zwykle) w dokumentacji , po prostu zastosowałem łatkę do instancji zamiast do klasy, kiedy poszedłem za ich przykładem.

Oto jak to zrobić:

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

W zestawie testowym:

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
źródło
14
ludzie powinni użyć innego przykładu. mock.PropertyMockjest na to sposób!
vitiral
4
Zgadza się, w momencie pisania tego tekstu PropertyMocknie było.
charlax
6

Jeśli obiekt, którego właściwość chcesz przesłonić, jest obiektem pozorowanym, nie musisz używać patch.

Zamiast tego można utworzyć, PropertyMocka następnie zastąpić właściwość typu makiety. Na przykład, aby przesłonić mock_rows.pageswłaściwość do zwrócenia (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
Tim Swast
źródło
1
Bam, po prostu to, czego chciałem (autospec'd obiekt z właściwością). I nie mniej od kolegi 🙋‍♂️
Mark McDonald
6

Pewnie to kwestia stylu, ale w przypadku wolisz dekoratorów w testach użytkownika @ jamescastlefield odpowiedź może być zmieniony na coś takiego:

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()
Eyal Levin
źródło
6

Jeśli używasz pytestrazem z pytest-mock, możesz uprościć swój kod, a także uniknąć używania menedżera kontekstu, tj. withInstrukcji w następujący sposób:

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
źródło
0

Jeśli nie chcesz testować, czy uzyskano dostęp do fałszywej właściwości, możesz po prostu załatać ją za pomocą oczekiwanego return_value.

with mock.patch(MyClass, 'last_transaction', Transaction()):
    ...
Simon Charette
źródło
0

Jeśli chcesz, aby twój wyśmienity @propertypolegał na oryginale __get__, możesz stworzyć swój własnyMockProperty

class PropertyMock(mock.Mock):

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

Stosowanie:

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
Conchylicultor
źródło