Python Mock - Patchen einer Methode, ohne die Implementierung zu behindern

73

Gibt es eine saubere Möglichkeit, ein Objekt zu patchen, damit Sie die assert_call*Helfer in Ihrem Testfall erhalten, ohne die Aktion tatsächlich zu entfernen?

Wie kann ich beispielsweise die @patchZeile ändern , um den folgenden Test zu bestehen:

from unittest import TestCase
from mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):

    @patch.object(Potato, 'foo')
    def test_something(self, mock):
        spud = Potato()
        forty_two = spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

Ich könnte dies wahrscheinlich zusammen mit hacken side_effect, aber ich hatte gehofft, dass es einen schöneren Weg gibt, der bei allen Funktionen, Klassenmethoden, statischen Methoden, ungebundenen Methoden usw. gleich funktioniert.

wim
quelle
Es macht auch nicht viel Sinn zu behaupten, dass fooaufgerufen wurde, da der Test selbst ihn aufruft und nicht irgendein anderer Code, der getestet wird. Ebenso scheint forty_twodas Testen , das von Ihrem Test auf einen bestimmten Wert festgelegt wird , und nicht der getestete Code, von geringem Wert zu sein.
Chepper
2
Es ist eine Sache. Tatsächlicher Code ist das Patchen von Instanzen, die in anderen Modulen erstellt wurden, tief verschachtelt usw.
wim
2
Ich habe die gleiche Frage; das Wichtigste für mich ist , dass die Lösung soll nicht von mir verlangt einen Code zwischen meiner Konstruktion der Instanz von Kartoffel (einzufügen spudin diesem Beispiel) und meine Berufung spud.foo. Ich muss spudvon fooAnfang an mit einer verspotteten Methode erstellt werden, da ich den Codepfad, der spuddie fooMethode erstellt und aufruft, nicht kontrolliere .
Quuxplusone
1
Als ich einige Jahre später darauf zurückkam, fand ich spy, dass es in meiner Pytest-Suite nützlich ist.
wim

Antworten:

59

Ähnliche Lösung mit Ihrer, aber mit wraps:

def test_something(self):
    spud = Potato()
    with patch.object(Potato, 'foo', wraps=spud.foo) as mock:
        forty_two = spud.foo(n=40)
        mock.assert_called_once_with(n=40)
    self.assertEqual(forty_two, 42)

Laut Dokumentation :

Wraps : Element, das das Scheinobjekt umbrechen soll. Wenn wraps nicht None ist, leitet der Aufruf des Mock den Aufruf an das umschlossene Objekt weiter (wobei das tatsächliche Ergebnis zurückgegeben wird). Der Attributzugriff auf das Modell gibt ein Mock-Objekt zurück, das das entsprechende Attribut des umschlossenen Objekts umschließt. Wenn Sie also versuchen, auf ein nicht vorhandenes Attribut zuzugreifen, wird ein AttributeError ausgelöst.


class Potato(object):

    def spam(self, n):
        return self.foo(n=n)

    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):

    def test_something(self):
        spud = Potato()
        with patch.object(Potato, 'foo', wraps=spud.foo) as mock:
            forty_two = spud.spam(n=40)
            mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)
falsetru
quelle
Danke, das ist etwas schöner als meins. Kennen Sie eine Möglichkeit, dies in der Verwendung des Patches durch den Dekorateur anstelle der Verwendung des Kontextmanagers zu tun?
wim
2
nein, denn wenn Sie eine neue Potato-Instanz im Dekorator erstellen, verlieren Sie den Status des Objekts, das gerade getestet wird. Sie benötigen die gebundene Methode.
wim
5
Ich frage mich, ob der Patch patch.object(spud, 'foo', wraps=spud.foo)stattdessen sein sollte, damit der Code die spezifische Instanz patchen würde. In diesem Fall macht es zwar keinen praktischen Unterschied, aber der aktuelle Code wird auf Klassenebene (alle Instanzen) gepatcht, umschließt jedoch eine an eine bestimmte Instanz gebundene Funktion. Ich denke, das könnte jemanden auf der Straße verbrennen.
Studgeek
1
@falsetru, ich sehe das, aber ich denke, der Klassen- / Instanzunterschied, wenn er von einem anderen SO-Reader verwendet wird, könnte jemanden mit diesem Beispiel verbrennen. Zum Beispiel, wenn im Testcode spud und spud2 mit unterschiedlichen Instanzwerten getestet wurden. Das Aufrufen von spud2.foo würde tatsächlich die Ergebnisse von spud.foo zurückgeben. Aus diesem Grund denke ich, dass das gepatchte Objekt eher Spud als seine Klasse sein sollte.
Studgeek
1
auch funktioniert dies gut mit der PyPI mockBibliothek für Python 2.7 (es von docs nicht sofort offensichtlich ist , als wrapsnicht ein dokumentiertes kwarg von ist patch.object, sondern es wird übergeben , wie **kwargsin MagicMockdem es wird dokumentiert)
Anentropic
13

Diese Antwort adressiert die zusätzliche Anforderung, die in der Prämie von Benutzer Quuxplusone erwähnt wird:

Das Wichtigste für meinen Anwendungsfall ist, dass es funktioniert @patch.mock, dh dass ich zwischen dem Erstellen der Instanz von Potato( spudin diesem Beispiel) und dem Aufrufen von keinen Code einfügen muss spud.foo. Ich muss spudvon Anfang an mit einer verspotteten fooMethode erstellt werden, da ich den Ort, an dem sie spuderstellt wird, nicht kontrolliere .

Der oben beschriebene Anwendungsfall konnte ohne allzu große Probleme mit einem Dekorateur erreicht werden:

import unittest
import unittest.mock  # Python 3

def spy_decorator(method_to_decorate):
    mock = unittest.mock.MagicMock()
    def wrapper(self, *args, **kwargs):
        mock(*args, **kwargs)
        return method_to_decorate(self, *args, **kwargs)
    wrapper.mock = mock
    return wrapper

def spam(n=42):
    spud = Potato()
    return spud.foo(n=n)

class Potato(object):

    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2

class PotatoTest(unittest.TestCase):

    def test_something(self):
        foo = spy_decorator(Potato.foo)
        with unittest.mock.patch.object(Potato, 'foo', foo):
            forty_two = spam(n=40)
        foo.mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)


if __name__ == '__main__':
    unittest.main()

Wenn die ersetzte Methode veränderbare Argumente akzeptiert, die während des Tests geändert werden, möchten Sie möglicherweise ein * anstelle des Inneren des spy_decorator initialisieren. CopyingMockMagicMock

* Es ist ein Rezept aus den Dokumenten, die ich auf PyPI als CopyingMock Lib veröffentlicht habe

wim
quelle
Ich habe noch nicht ganz verstanden, was Wim hier macht, aber "Danke" - ich habe den ganzen Nachmittag meinen Kopf gegen diese Mauer geschlagen!
Paul D Smith