Wie verspottet man eine schreibgeschützte Eigenschaft mit mock?

91

Wie verspottet man eine schreibgeschützte Eigenschaft mit mock ?

Ich habe es versucht:

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

aber das Problem ist, dass es dann für alle Instanzen der Klasse gilt ... was meine Tests bricht.

Hast du eine andere Idee? Ich möchte nicht das gesamte Objekt verspotten, sondern nur diese bestimmte Eigenschaft.

charlax
quelle

Antworten:

161

Ich denke, der bessere Weg ist, die Eigenschaft PropertyMockals zu verspotten, als die __get__Methode direkt zu verspotten .

In der Dokumentation wird Folgendes angegeben unittest.mock.PropertyMock: Suchen Sie nach : Ein Modell, das als Eigenschaft oder anderer Deskriptor für eine Klasse verwendet werden soll. PropertyMockstellt __get__und __set__Methoden bereit , mit denen Sie beim Abrufen einen Rückgabewert angeben können.

Hier ist, wie:

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
quelle
Ich musste mich über eine Klassenmethode lustig machen, die als dekoriert war @property. Diese Antwort hat bei mir funktioniert, wenn die andere Antwort (und andere Antworten auf viele andere Fragen) nicht funktioniert hat.
AlanSE
2
So sollte es gemacht werden. Ich wünschte, es gäbe eine Möglichkeit, die "akzeptierte" Antwort zu verschieben
vitiral
4
Ich finde das Einfügen des Rückgabewerts in den Kontextmanager-Aufruf etwas sauberer: `` `mit mock.patch ('MyClass.last_transaction', new_callable = PropertyMock, return_value = Transaction ()): ...` ``
wodow
In der Tat habe ich gerade die akzeptierte Antwort auf diese verschoben.
Charlax
1
Die Verwendung von mock.patch.object ist auch hilfreich, da Sie den Klassennamen nicht als Zeichenfolge schreiben müssen (im Beispiel kein wirkliches Problem) und es einfacher zu erkennen / zu beheben ist, wenn Sie ein Paket umbenennen möchten und dies nicht getan haben hat einen Test aktualisiert
Kevin
41

Eigentlich war die Antwort (wie üblich) in der Dokumentation , es ist nur so, dass ich den Patch auf die Instanz anstatt auf die Klasse angewendet habe, als ich ihrem Beispiel gefolgt bin.

So geht's:

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

In der Testsuite:

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
quelle
14
Leute sollten das andere Beispiel verwenden. mock.PropertyMockist der Weg es zu tun!
Vitiral
4
Das ist richtig, zum Zeitpunkt des Schreibens PropertyMockgab es noch keine.
Charlax
6

Wenn das Objekt, dessen Eigenschaft Sie überschreiben möchten, ein Scheinobjekt ist, müssen Sie es nicht verwenden patch.

Kann stattdessen eine erstellen PropertyMockund dann die Eigenschaft für den Typ des Mocks überschreiben . So überschreiben Sie beispielsweise die zurückzugebende mock_rows.pagesEigenschaft (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
quelle
1
Bam, genau das, was ich wollte (autospec'd Objekt mit einer Eigenschaft). Und von einem Kollegen nicht weniger Mark♂️
Mark McDonald
6

Wahrscheinlich eine Frage des Stils, aber falls Sie Dekorateure in Tests bevorzugen, könnte die Antwort von @ jamescastlefield wie folgt geändert werden:

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
quelle
5

Wenn Sie pytestzusammen mit verwenden pytest-mock, können Sie Ihren Code vereinfachen und die Verwendung des Kontextmanagers, dh der folgenden withAnweisung, vermeiden :

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
quelle
0

Wenn Sie nicht testen möchten, ob auf die verspottete Eigenschaft zugegriffen wurde, können Sie sie einfach mit der erwarteten Eigenschaft patchen return_value.

with mock.patch(MyClass, 'last_transaction', Transaction()):
    ...
Simon Charette
quelle