Stellen Sie sicher, dass eine Methode in einem Python-Komponententest aufgerufen wurde

90

Angenommen, ich habe den folgenden Code in einem Python-Komponententest:

aw = aps.Request("nv1")
aw2 = aps.Request("nv2", aw)

Gibt es eine einfache Möglichkeit zu behaupten, dass eine bestimmte Methode (in meinem Fall aw.Clear()) in der zweiten Zeile des Tests aufgerufen wurde? zB gibt es so etwas:

#pseudocode:
assertMethodIsCalled(aw.Clear, lambda: aps.Request("nv2", aw))
Mark Heath
quelle

Antworten:

148

Ich benutze Mock (das ist jetzt unittest.mock auf py3.3 +) dafür:

from mock import patch
from PyQt4 import Qt


@patch.object(Qt.QMessageBox, 'aboutQt')
def testShowAboutQt(self, mock):
    self.win.actionAboutQt.trigger()
    self.assertTrue(mock.called)

Für Ihren Fall könnte es so aussehen:

import mock
from mock import patch


def testClearWasCalled(self):
   aw = aps.Request("nv1")
   with patch.object(aw, 'Clear') as mock:
       aw2 = aps.Request("nv2", aw)

   mock.assert_called_with(42) # or mock.assert_called_once_with(42)

Mock unterstützt einige nützliche Funktionen, einschließlich Möglichkeiten zum Patchen eines Objekts oder Moduls sowie zum Überprüfen, ob das Richtige aufgerufen wurde, usw. usw.

Vorbehalt Emptor! (Käufer aufgepasst!)

Wenn Sie assert_called_with(zu assert_called_onceoder assert_called_wiht) falsch tippen , wird Ihr Test möglicherweise noch ausgeführt, da Mock dies für eine verspottete Funktion hält und gerne mitmacht , es sei denn, Sie verwenden autospec=true. Weitere Informationen finden Sie unter assert_called_once: Bedrohung oder Bedrohung .

Macke
quelle
5
+1 für die diskrete Erleuchtung meiner Welt mit dem wunderbaren Mock-Modul.
Ron Cohen
@ RonCohen: Ja, es ist ziemlich erstaunlich und wird auch immer besser. :)
Macke
1
Während die Verwendung von Mock definitiv der richtige Weg ist, würde ich davon abraten, assert_called_once zu verwenden, da es einfach nicht existiert :)
FelixCQ
Es wurde in späteren Versionen entfernt. Meine Tests verwenden es immer noch. :)
Macke
1
Es lohnt sich zu wiederholen, wie hilfreich es ist, autospec = True für jedes verspottete Objekt zu verwenden, da es Sie wirklich beißen kann, wenn Sie die Assert-Methode falsch schreiben.
Rgilligan
30

Ja, wenn Sie Python 3.3+ verwenden. Sie können die integrierte unittest.mockMethode verwenden, um die aufgerufene Methode zu bestätigen. Verwenden Sie für Python 2.6+ den rollierenden Backport Mock, der dasselbe ist.

Hier ist ein kurzes Beispiel in Ihrem Fall:

from unittest.mock import MagicMock
aw = aps.Request("nv1")
aw.Clear = MagicMock()
aw2 = aps.Request("nv2", aw)
assert aw.Clear.called
Devy
quelle
14

Mir ist nichts Eingebautes bekannt. Es ist ziemlich einfach zu implementieren:

class assertMethodIsCalled(object):
    def __init__(self, obj, method):
        self.obj = obj
        self.method = method

    def called(self, *args, **kwargs):
        self.method_called = True
        self.orig_method(*args, **kwargs)

    def __enter__(self):
        self.orig_method = getattr(self.obj, self.method)
        setattr(self.obj, self.method, self.called)
        self.method_called = False

    def __exit__(self, exc_type, exc_value, traceback):
        assert getattr(self.obj, self.method) == self.called,
            "method %s was modified during assertMethodIsCalled" % self.method

        setattr(self.obj, self.method, self.orig_method)

        # If an exception was thrown within the block, we've already failed.
        if traceback is None:
            assert self.method_called,
                "method %s of %s was not called" % (self.method, self.obj)

class test(object):
    def a(self):
        print "test"
    def b(self):
        self.a()

obj = test()
with assertMethodIsCalled(obj, "a"):
    obj.b()

Dies erfordert, dass das Objekt selbst self.b nicht ändert, was fast immer der Fall ist.

Glenn Maynard
quelle
Ich sagte, mein Python sei verrostet, obwohl ich meine Lösung getestet habe, um sicherzustellen, dass sie funktioniert :-) Ich habe Python vor Version 2.5 verinnerlicht. Tatsächlich habe ich 2.5 für kein signifikantes Python verwendet, da wir aus Gründen der Lib-Kompatibilität bei 2.3 einfrieren mussten. Bei der Überprüfung Ihrer Lösung fand ich effbot.org/zone/python-with-statement.htm als eine schöne klare Beschreibung. Ich würde demütig vorschlagen, dass mein Ansatz kleiner aussieht und möglicherweise einfacher anzuwenden ist, wenn Sie mehr als einen Protokollierungspunkt möchten, anstatt "mit" s zu verschachteln. Ich möchte wirklich, dass Sie erklären, ob Sie besondere Vorteile haben.
Andy Dent
@Andy: Ihre Antwort ist kleiner, weil sie teilweise ist: Sie testet die Ergebnisse nicht, stellt die ursprüngliche Funktion nach dem Test nicht wieder her, sodass Sie das Objekt weiterhin verwenden können, und Sie müssen den Code wiederholt schreiben, um alles zu tun das jedes Mal, wenn Sie einen Test schreiben. Die Anzahl der Zeilen des Support-Codes ist nicht wichtig. Diese Klasse befindet sich in einem eigenen Testmodul, nicht in einer Dokumentzeichenfolge - dies erfordert ein oder zwei Codezeilen im eigentlichen Test.
Glenn Maynard
6

Ja, ich kann Ihnen den Umriss geben, aber mein Python ist etwas verrostet und ich bin zu beschäftigt, um es im Detail zu erklären.

Grundsätzlich müssen Sie einen Proxy in die Methode einfügen, der das Original aufruft, z.

 class fred(object):
   def blog(self):
     print "We Blog"


 class methCallLogger(object):
   def __init__(self, meth):
     self.meth = meth

   def __call__(self, code=None):
     self.meth()
     # would also log the fact that it invoked the method

 #example
 f = fred()
 f.blog = methCallLogger(f.blog)

Diese StackOverflow-Antwort zu callable kann Ihnen helfen, die oben genannten Punkte zu verstehen.

Genauer:

Obwohl die Antwort aufgrund der interessanten Diskussion mit Glenn und der Tatsache, dass ich ein paar Minuten Zeit hatte, akzeptiert wurde, wollte ich meine Antwort erweitern:

# helper class defined elsewhere
class methCallLogger(object):
   def __init__(self, meth):
     self.meth = meth
     self.was_called = False

   def __call__(self, code=None):
     self.meth()
     self.was_called = True

#example
class fred(object):
   def blog(self):
     print "We Blog"

f = fred()
g = fred()
f.blog = methCallLogger(f.blog)
g.blog = methCallLogger(g.blog)
f.blog()
assert(f.blog.was_called)
assert(not g.blog.was_called)
Andy Dent
quelle
nett. Ich habe methCallLogger eine Anrufanzahl hinzugefügt, damit ich dies bestätigen kann.
Mark Heath
Dies über die gründliche, in sich geschlossene Lösung, die ich bereitgestellt habe? Ernsthaft?
Glenn Maynard
@Glenn Ich bin sehr neu in Python - vielleicht ist deine besser - ich verstehe nur noch nicht alles. Ich werde später ein bisschen Zeit damit verbringen, es auszuprobieren.
Mark Heath
Dies ist bei weitem die einfachste und am einfachsten zu verstehende Antwort. Wirklich gute Arbeit!
Matt Messersmith
4

Sie können aw.Clearentweder manuell oder mit einem Testframework wie pymox verspotten . Manuell würden Sie es mit so etwas tun:

class MyTest(TestCase):
  def testClear():
    old_clear = aw.Clear
    clear_calls = 0
    aw.Clear = lambda: clear_calls += 1
    aps.Request('nv2', aw)
    assert clear_calls == 1
    aw.Clear = old_clear

Mit Pymox würden Sie es so machen:

class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)
Max Shawabkeh
quelle
Ich mag diesen Ansatz auch, obwohl ich immer noch möchte, dass old_clear aufgerufen wird. Dies macht deutlich, was los ist.
Mark Heath