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

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 .


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

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ą.


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)

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

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.