¿Cómo me burlo de un controlador de señal django?

Tengo un signal_handler conectado a través de un decorador, algo así como este muy simple:

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

Lo que quiero hacer es burlarse de ella con la biblioteca simulada http://www.voidspace.org.uk/python/mock/ en una prueba, para verificar cuántas veces lo llama Django. Mi código en este momento es algo como:

 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) 

El problema aquí es que el controlador de señal original se llama incluso si está @receiver , probablemente porque el decorador @receiver está almacenando una copia del controlador de señal en algún lugar, así que me estoy burlando del código incorrecto.

Entonces la pregunta: ¿cómo me burlo de mi controlador de señales para hacer que mi prueba funcione?

Tenga en cuenta que si cambio mi controlador de señal 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) 

y me burlo de _support_function en _support_function lugar, todo funciona como se esperaba.

Entonces, terminé con una especie de solución: burlarse de un manejador de señal simplemente significa conectar el simulacro a la señal, así que esto es exactamente lo que hice:

 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 

Tenga en mock.patch se requiere autospec=True in mock.patch para que post_save.connect funcione correctamente en un MagicMock , de lo contrario, django generará algunas excepciones y la conexión fallará.

Posiblemente una mejor idea es simular la funcionalidad dentro del manejador de señales en lugar del manejador en sí mismo. Utilizando el código del OP:

 @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 

Entonces se burla do_stuff :

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

Echa un vistazo a mock_django. Tiene soporte para señales.

https://github.com/dcramer/mock-django/blob/master/tests/mock_django/signals/tests.py

Hay una manera de burlarse de las señales de Django con una clase pequeña.

Debe tener en cuenta que esto solo se burlaría de la función como un controlador de señal django y no como la función original; por ejemplo, si un m2mchange desencadena una llamada a una función que llama directamente a su manejador, mock.call_count no se incrementaría. Necesitaría un simulacro separado para realizar un seguimiento de esas llamadas.

Aquí está la clase en cuestión:

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

Ejemplo de uso

 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) 

Puede simular una señal django simulando la clase django.db.models.signals.py en django.db.models.signals.py esta manera:

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

Eso debería hacer el truco. Tenga en cuenta que esto simulará TODAS las señales sin importar qué objeto esté utilizando.

Si por casualidad usas la biblioteca de mocker lugar, se puede hacer así:

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

Es más líneas pero también funciona bastante bien 🙂

En Django 1.9 puedes burlarte de todos los receptores con algo como esto.

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

Esto reemplaza todos los receptores con simulacros, por ejemplo, los que ha registrado, las aplicaciones conectables se han registrado y las que el propio django ha registrado. No se sorprenda si usa esto en post_save y las cosas comienzan a romperse.

Es posible que desee inspeccionar el receptor para determinar si realmente quiere burlarse de él.