Asserire che un metodo è stato chiamato in uno unit test di Python


95

Supponiamo di avere il seguente codice in uno unit test Python:

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

C'è un modo semplice per affermare che un metodo particolare (nel mio caso aw.Clear()) è stato chiamato durante la seconda riga del test? ad es. c'è qualcosa di simile:

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

Risposte:


153

Uso Mock (che ora è unittest.mock su py3.3 +) per questo:

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)

Per il tuo caso, potrebbe assomigliare a questo:

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 supporta alcune funzioni utili, inclusi i modi per patchare un oggetto o un modulo, oltre a controllare che sia stata chiamata la cosa giusta, ecc.

Caveat emptor! (Compratore stai attento!)

Se digiti in modo errato assert_called_with( assert_called_onceo assert_called_wiht) il tuo test potrebbe ancora essere eseguito, poiché Mock penserà che questa sia una funzione derisa e andrà avanti felicemente, a meno che tu non usi autospec=true. Per maggiori informazioni leggi assert_called_once: Threat or Menace .


5
+1 per aver illuminato discretamente il mio mondo con il meraviglioso modulo Mock.
Ron Cohen,

@RonCohen: Sì, è piuttosto sorprendente, e anche migliorando continuamente. :)
Macke

1
Sebbene l'uso di mock è sicuramente la strada da percorrere, sconsiglierei di usare assert_called_once, con semplicemente non esiste :)
FelixCQ

è stato rimosso nelle versioni successive. I miei test lo stanno ancora usando. :)
Macke

1
Vale la pena ripetere quanto sia utile usare autospec = True per qualsiasi oggetto deriso perché può davvero morderti se sbagli il metodo di asserzione.
rgilligan

31

Sì, se stai usando Python 3.3+. È possibile utilizzare l'integrato unittest.mockper affermare il metodo chiamato. Per Python 2.6+ usa il backport a rotazione Mock, che è la stessa cosa.

Ecco un rapido esempio nel tuo caso:

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

14

Non sono a conoscenza di nulla di integrato. È abbastanza semplice da implementare:

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

Ciò richiede che l'oggetto stesso non modifichi self.b, il che è quasi sempre vero.


Ho detto che il mio Python era arrugginito, anche se ho testato la mia soluzione per assicurarmi che funzioni :-) Ho interiorizzato Python prima della versione 2.5, infatti non ho mai usato 2.5 per nessun Python significativo poiché dovevamo congelare a 2.3 per compatibilità con lib. Nel rivedere la tua soluzione ho trovato effbot.org/zone/python-with-statement.htm come una bella descrizione chiara. Vorrei umilmente suggerire che il mio approccio sembra più piccolo e potrebbe essere più facile da applicare se volessi più di un punto di registrazione, piuttosto che annidato "con" s. Vorrei davvero che mi spiegassi se ci sono vantaggi particolari da parte sua.
Andy Dent

@ Anddy: la tua risposta è più piccola perché è parziale: in realtà non verifica i risultati, non ripristina la funzione originale dopo il test, quindi puoi continuare a utilizzare l'oggetto e devi scrivere ripetutamente il codice per fare tutto di nuovo ogni volta che scrivi un test. Il numero di righe di codice di supporto non è importante; questa classe va nel proprio modulo di test, non inline in una docstring - questo richiede una o due righe di codice nel test vero e proprio.
Glenn Maynard

6

Sì, posso darti lo schema ma il mio Python è un po 'arrugginito e sono troppo occupato per spiegarlo in dettaglio.

Fondamentalmente, è necessario inserire un proxy nel metodo che chiamerà l'originale, ad esempio:

 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)

Questa risposta StackOverflow su callable può aiutarti a capire quanto sopra.

Più in dettaglio:

Sebbene la risposta sia stata accettata, vista l'interessante discussione con Glenn e avendo qualche minuto libero, ho voluto ampliare la mia risposta:

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

simpatico. Ho aggiunto un conteggio delle chiamate a methCallLogger in modo da poterlo affermare.
Mark Heath

Questo per la soluzione completa e autonoma che ho fornito? Sul serio?
Glenn Maynard,

@ Glenn Sono molto nuovo su Python - forse il tuo è migliore - non lo capisco ancora tutto. Passerò un po 'di tempo dopo a provarlo.
Mark Heath

Questa è di gran lunga la risposta più semplice e di facile comprensione. Davvero un bel lavoro!
Matt Messersmith

4

Puoi eseguire il mock-out aw.Clear, manualmente o utilizzando un framework di test come pymox . Manualmente, lo faresti usando qualcosa del genere:

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

Usando pymox, lo faresti in questo modo:

class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)

Mi piace anche questo approccio, anche se voglio che old_clear venga chiamato. Questo rende ovvio cosa sta succedendo.
Mark Heath
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.