2012-10-28 21 views
33

Ho un signal_handler collegato tramite un decoratore, qualcosa di simile molto semplice:Come faccio a prendere in giro un gestore di segnale django?

@receiver(post_save, sender=User, 
      dispatch_uid='myfile.signal_handler_post_save_user') 
def signal_handler_post_save_user(sender, *args, **kwargs): 
    # do stuff 

Quello che voglio fare è prendere in giro lo con la libreria finta http://www.voidspace.org.uk/python/mock/ in un test, per verificare quante volte Django lo chiama. Il mio codice in questo momento è qualcosa di simile:

def test_cache(): 
    with mock.patch('myapp.myfile.signal_handler_post_save_user') as mocked_handler: 
     # do stuff that will call the post_save of User 
    self.assert_equal(mocked_handler.call_count, 1) 

Il problema qui è che il gestore del segnale originale è chiamato anche se preso in giro, molto probabilmente perché il decoratore @receiver memorizza una copia del gestore di segnale da qualche parte, così ho' Sto deridendo il codice sbagliato.

Quindi la domanda: come faccio a prendere in giro il mio gestore di segnale per far funzionare il mio test?

Si noti che se cambio gestore del segnale a:

def _support_function(*args, **kwargs): 
    # do stuff 

@receiver(post_save, sender=User, 
      dispatch_uid='myfile.signal_handler_post_save_user') 
def signal_handler_post_save_user(sender, *args, **kwargs): 
    _support_function(*args, **kwargs) 

e mi prendo gioco _support_function invece, tutto funziona come previsto.

risposta

14

Così, ho finito con una sorta-di soluzione: beffardo un gestore di segnale significa semplicemente collegare il mock stesso per il segnale, quindi questo è esattamente quello che ho fatto:

def test_cache(): 
    with mock.patch('myapp.myfile.signal_handler_post_save_user', autospec=True) as mocked_handler: 
     post_save.connect(mocked_handler, sender=User, dispatch_uid='test_cache_mocked_handler') 
     # do stuff that will call the post_save of User 
    self.assertEquals(mocked_handler.call_count, 1) # standard django 
    # self.assert_equal(mocked_handler.call_count, 1) # when using django-nose 

noti che autospec=True in mock.patch è necessaria al fine di rendere post_save.connect per funzionare correttamente su un MagicMock, altrimenti Django sollevare alcune eccezioni e la connessione avrà esito negativo.

+1

non dovrebbe 'assert_equal' essere' assertEquals (...) '? –

+2

Dipende dalla suite di test che si sta utilizzando; django di default usa 'unittest', che ha 'assertEquals' come dici tu; Io uso sempre il "naso" che, secondo me, è superiore in molti aspetti, e il naso ha un "assert_equal". Quando scrivo la mia risposta, copia/incolla dal mio codice di produzione, ecco perché vedi 'assert_equal' lì. Ho modificato la risposta per rispettare il default di Django, grazie per aver segnalato questo – StefanoP

+0

grazie! Uso anche il naso ma non ho mai realizzato che esistesse assert_equal –

2

C'è un modo per deridere i segnali django con una piccola classe.

Si dovrebbe tenere presente che questo non farebbe che prendere in giro la funzione come gestore del segnale django e non la funzione originale; ad esempio, se m2mchange attiva una chiamata a una funzione che chiama direttamente il gestore, mock.call_count non verrebbe incrementato. Avresti bisogno di una simulazione separata per tenere traccia di quelle chiamate.

Qui è la classe in questione:

class LocalDjangoSignalsMock(): 
    def __init__(self, to_mock): 
     """ 
     Replaces registered django signals with MagicMocks 

     :param to_mock: list of signal handlers to mock 
     """ 
     self.mocks = {handler:MagicMock() for handler in to_mock} 
     self.reverse_mocks = {magicmock:mocked 
           for mocked,magicmock in self.mocks.items()} 
     django_signals = [signals.post_save, signals.m2m_changed] 
     self.registered_receivers = [signal.receivers 
            for signal in django_signals] 

    def _apply_mocks(self): 
     for receivers in self.registered_receivers: 
      for receiver_index in xrange(len(receivers)): 
       handler = receivers[receiver_index] 
       handler_function = handler[1]() 
       if handler_function in self.mocks: 
        receivers[receiver_index] = (
         handler[0], self.mocks[handler_function]) 

    def _reverse_mocks(self): 
     for receivers in self.registered_receivers: 
      for receiver_index in xrange(len(receivers)): 
       handler = receivers[receiver_index] 
       handler_function = handler[1] 
       if not isinstance(handler_function, MagicMock): 
        continue 
       receivers[receiver_index] = (
        handler[0], weakref.ref(self.reverse_mocks[handler_function])) 

    def __enter__(self): 
     self._apply_mocks() 
     return self.mocks 

    def __exit__(self, *args): 
     self._reverse_mocks() 

Esempio di utilizzo

to_mock = [my_handler] 
with LocalDjangoSignalsMock(to_mock) as mocks: 
    my_trigger() 
    for mocked in to_mock: 
     assert(mocks[mocked].call_count) 
     # 'function {0} was called {1}'.format(
     #  mocked, mocked.call_count) 
1

È possibile prendere in giro un segnale di Django deridendo la classe ModelSignal a django.db.models.signals.py come questo:

@patch("django.db.models.signals.ModelSignal.send") 
def test_overwhelming(self, mocker_signal): 
    obj = Object() 

Questo dovrebbe fare il trucco. Nota che questo simulerà TUTTI i segnali indipendentemente dall'oggetto che stai usando.

Se per caso si utilizza la libreria mocker invece, si può fare in questo modo:

from mocker import Mocker, ARGS, KWARGS 

def test_overwhelming(self): 
    mocker = Mocker() 
    # mock the post save signal 
    msave = mocker.replace("django.db.models.signals") 
    msave.post_save.send(KWARGS) 
    mocker.count(0, None) 

    with mocker: 
     obj = Object() 

E 'più linee ma funziona abbastanza bene anche :)

9

Forse una migliore idea è quella di prendere in giro la funzionalità all'interno di il gestore di segnale piuttosto che il gestore stesso. Utilizzando il codice del PO:

@receiver(post_save, sender=User, dispatch_uid='myfile.signal_handler_post_save_user') 
def signal_handler_post_save_user(sender, *args, **kwargs): 
    do_stuff() # <-- mock this 

def do_stuff(): 
    ... do stuff in here 

Poi deridere do_stuff:

with mock.patch('myapp.myfile.do_stuff') as mocked_handler: 
    self.assert_equal(mocked_handler.call_count, 1) 
1

In Django 1.9 si può prendere in giro tutti i ricevitori con qualcosa di simile

# replace actual receivers with mocks 
mocked_receivers = [] 
for i, receiver in enumerate(your_signal.receivers): 
    mock_receiver = Mock() 
    your_signal.receivers[i] = (receiver[0], mock_receiver) 
    mocked_receivers.append(mock_receiver) 

... # whatever your test does 

# ensure that mocked receivers have been called as expected 
for mocked_receiver in mocked_receivers: 
    assert mocked_receiver.call_count == 1 
    mocked_receiver.assert_called_with(*your_args, sender="your_sender", signal=your_signal, **your_kwargs) 

Questo sostituisce tutti i ricevitori con prende in giro, ad esempio, quelli che hai registrato, quelli che le app collegabili hanno registrato e quelli che lo stesso Django ha registrato. Non essere sorpreso se lo usi su post_save e le cose iniziano a rompersi.

Si consiglia di ispezionare il ricevitore per determinare se si vuole effettivamente deriderlo.

Problemi correlati