Potwierdź, że metoda została wywołana w teście jednostkowym języka Python

91

Załóżmy, że mam następujący kod w teście jednostkowym w Pythonie:

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

Czy jest łatwy sposób na stwierdzenie, że dana metoda (w moim przypadku aw.Clear()) została wywołana w drugiej linii testu? np. czy jest coś takiego:

#pseudocode:
assertMethodIsCalled(aw.Clear, lambda: aps.Request("nv2", aw))
Mark Heath
źródło

Odpowiedzi:

150

Używam Mocka (który jest teraz unittest.mock na py3.3 +) do tego:

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)

W twoim przypadku może to wyglądać tak:

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 obsługuje wiele przydatnych funkcji, w tym sposoby łatania obiektu lub modułu, a także sprawdzanie, czy została wywołana właściwa rzecz itp.

Caveat emptor! (Kupujący, strzeż się!)

Jeśli błędnie wpiszesz assert_called_with(do assert_called_oncelub assert_called_wiht), twój test może nadal działać, ponieważ Mock pomyśli, że jest to wyszydzona funkcja i z radością pójdzie dalej, chyba że użyjesz autospec=true. Aby uzyskać więcej informacji, przeczytaj assert_called_once: Threat or Menace .

Macke
źródło
5
+1 za dyskretne oświecenie mojego świata za pomocą wspaniałego modułu Mock.
Ron Cohen
@RonCohen: Tak, jest niesamowity i cały czas jest coraz lepszy. :)
Macke
1
Chociaż używanie makiety jest zdecydowanie najlepszym rozwiązaniem, odradzam używanie assert_called_once, które po prostu nie istnieje :)
FelixCQ
został usunięty w późniejszych wersjach. Moje testy nadal go używają. :)
Macke
1
Warto powtórzyć, jak pomocne jest użycie autospec = True dla każdego wyszydzonego obiektu, ponieważ może naprawdę cię ugryźć, jeśli źle wpiszesz metodę assert.
rgilligan,
30

Tak, jeśli używasz Pythona 3.3+. Możesz użyć wbudowanej unittest.mockmetody assert o nazwie. W przypadku Pythona 2.6+ użyj cofania wstecznego Mock, co jest tym samym.

Oto krótki przykład w Twoim przypadku:

from unittest.mock import MagicMock
aw = aps.Request("nv1")
aw.Clear = MagicMock()
aw2 = aps.Request("nv2", aw)
assert aw.Clear.called
Devy
źródło
14

Nie jestem świadomy niczego wbudowanego. Wdrożenie jest dość proste:

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()

Wymaga to, aby sam obiekt nie modyfikował self.b, co prawie zawsze jest prawdą.

Glenn Maynard
źródło
Powiedziałem, że mój Python jest zardzewiały, chociaż przetestowałem moje rozwiązanie, aby upewnić się, że działa :-) Zinternalizowałem Pythona przed wersją 2.5, w rzeczywistości nigdy nie używałem 2.5 dla żadnego znaczącego Pythona, ponieważ musieliśmy zawiesić na 2.3 dla kompatybilności z lib. Przeglądając twoje rozwiązanie, znalazłem effbot.org/zone/python-with-statement.htm jako ładny, jasny opis. Pokornie zasugerowałbym, że moje podejście wygląda na mniejsze i mogłoby być łatwiejsze do zastosowania, gdybyś chciał mieć więcej niż jeden punkt logowania, a nie zagnieżdżony „z”. Naprawdę chciałbym, żebyś wyjaśnił, czy są jakieś szczególne korzyści z tego.
Andy Dent
@Andy: Twoja odpowiedź jest mniejsza, ponieważ jest częściowa: w rzeczywistości nie testuje wyników, nie przywraca oryginalnej funkcji po teście, więc możesz kontynuować korzystanie z obiektu, i musisz wielokrotnie pisać kod, aby zrobić wszystko to znowu za każdym razem, gdy piszesz test. Liczba linii kodu pomocniczego nie jest ważna; ta klasa jest umieszczana we własnym module testowym, a nie w ciągach dokumentów - w tym teście zajmuje jeden lub dwa wiersze kodu.
Glenn Maynard
6

Tak, mogę podać zarys, ale mój Python jest trochę zardzewiały i jestem zbyt zajęty, aby szczegółowo wyjaśniać.

Zasadniczo musisz umieścić proxy w metodzie, która wywoła oryginał, np:

 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)

Ta odpowiedź StackOverflow na temat połączeń wywoływanych może pomóc ci zrozumieć powyższe.

Bardziej szczegółowo:

Chociaż odpowiedź została przyjęta, ze względu na ciekawą dyskusję z Glennem i kilka wolnych minut, chciałem rozszerzyć moją odpowiedź:

# 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
źródło
ładny. Dodałem liczbę połączeń do methCallLogger, więc mogę to potwierdzić.
Mark Heath
To ponad dokładne, samodzielne rozwiązanie, które dostarczyłem? Poważnie?
Glenn Maynard,
@Glenn Jestem bardzo nowy w Pythonie - może twój jest lepszy - po prostu jeszcze go nie rozumiem. Spędzę trochę czasu później wypróbowując to.
Mark Heath
To zdecydowanie najprostsza i najłatwiejsza do zrozumienia odpowiedź. Naprawdę fajna robota!
Matt Messersmith
4

Możesz wyszydzać aw.Clear, ręcznie lub używając frameworka testowego, takiego jak pymox . Ręcznie zrobiłbyś to za pomocą czegoś takiego:

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

Używając pymox, zrobiłbyś to w ten sposób:

class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)
Max Shawabkeh
źródło
Też mi się podoba to podejście, chociaż nadal chcę, aby zadzwoniono do old_clear. To sprawia, że ​​jest oczywiste, co się dzieje.
Mark Heath