Подтвердить что ты не робот

Как мне высмеять обработчик сигнала 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, все работает как ожидалось.

4b9b3361

Ответ 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)

Ответ 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