Как мне высмеять обработчик сигнала django?
У меня есть signal_handler, подключенный через декоратор, что-то вроде этого очень простого:
@receiver(post_save, sender=User,
dispatch_uid='myfile.signal_handler_post_save_user')
def signal_handler_post_save_user(sender, *args, **kwargs):
# do stuff
Что я хочу сделать, это издеваться над ним с макетной библиотекой http://www.voidspace.org.uk/python/mock/ в тесте, чтобы проверить, сколько раз django называет это. Мой код на данный момент выглядит примерно так:
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)
Проблема заключается в том, что исходный обработчик сигнала вызывается, даже если высмеивается, скорее всего, потому что декодер @receiver
хранит копию обработчика сигнала где-то, поэтому я издеваюсь над неправильным кодом.
Итак, вопрос: как мне высмеять мой обработчик сигнала, чтобы моя тестовая работа?
Обратите внимание, что если я изменил свой обработчик сигнала на:
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)
и я притворяюсь _support_function
, все работает как ожидалось.
Ответы
Ответ 1
Итак, у меня получилось какое-то решение: издевательство над обработчиком сигнала просто означает подключить сам макет к сигналу, так что это именно то, что я сделал:
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
Обратите внимание, что autospec=True
в mock.patch
требуется, чтобы post_save.connect
корректно работало над MagicMock
, иначе django приведет к некоторым исключениям и соединение завершится с ошибкой.
Ответ 2
Возможно, лучшая идея состоит в том, чтобы высмеять функциональность внутри обработчика сигнала, а не самого обработчика. Использование кода 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
Затем mock do_stuff
:
with mock.patch('myapp.myfile.do_stuff') as mocked_handler:
self.assert_equal(mocked_handler.call_count, 1)
Ответ 3
взгляните на mock_django. Он поддерживает сигналы
https://github.com/dcramer/mock-django/blob/master/tests/mock_django/signals/tests.py
Ответ 4
Существует способ измельчения сигналов django с небольшим классом.
Вы должны иметь в виду, что это только высмеивает функцию как обработчик сигнала django, а не оригинальную функцию; например, если m2mchange инициирует вызов функции, вызывающей ваш обработчик напрямую, mock.call_count не будет увеличиваться. Для отслеживания этих вызовов вам понадобится отдельный макет.
Вот класс, о котором идет речь:
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()
Пример использования
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)
Ответ 5
Вы можете издеваться над сигналом django, высмеивая класс ModelSignal в django.db.models.signals.py
следующим образом:
@patch("django.db.models.signals.ModelSignal.send")
def test_overwhelming(self, mocker_signal):
obj = Object()
Это должно сделать трюк. Обратите внимание, что это будет издеваться над всеми сигналами независимо от того, какой объект вы используете.
Если вы случайно используете библиотеку mocker
, это можно сделать следующим образом:
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()
Это больше строк, но оно тоже очень хорошо работает:)
Ответ 6
В django 1.9 вы можете издеваться над всеми получателями с чем-то вроде этого
# 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)
Это заменяет всех получателей на mocks, например зарегистрированные вами зарегистрированные приложения, и те, которые зарегистрировали сам django. Не удивляйтесь, если вы используете это на post_save
, и все начинает ломаться.
Вы можете проверить приемник, чтобы определить, хотите ли вы его высмеять.
Ответ 7
Как вы упомянули, mock.patch('myapp.myfile._support_function')
является правильным, а mock.patch('myapp.myfile.signal_handler_post_save_user')
неверным.
Я думаю, что причина в том, что:
Когда вы тестируете init, какой-то файл импортирует файл python реализации сигнала, затем декоратор @receive
создает новое соединение сигнала.
В тесте mock.patch('myapp.myfile._support_function')
создаст другое сигнальное соединение, поэтому оригинальный обработчик сигнала вызывается, даже если имитируется.
Попробуйте отключить сигнальное соединение перед mock.patch('myapp.myfile._support_function')
, например
post_save.disconnect(signal_handler_post_save_user)
with mock.patch("review.signals. signal_handler_post_save_user", autospec=True) as handler:
#do stuff