web-dev-qa-db-ja.com

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をモックしますが、すべてが期待どおりに機能します。

45
StefanoP

だから、私は一種の解決策に行き着きました:シグナルハンドラーをモックすることは、単にモック自体をシグナルに接続することを意味するので、これはまさに私がしたことです:

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=TrueMagicMockで正しく機能させるには、mock.patchpost_save.connectが必要であることに注意してください。そうでない場合、Django例外が発生すると、接続は失敗します。

18
StefanoP

おそらく、より良いアイデアは、機能をモックアウトすることですinsideハンドラー自体ではなくシグナルハンドラー。 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

次に、モックdo_stuff

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

mock_Djangoを見てください。シグナルをサポートしています

https://github.com/dcramer/mock-Django/blob/master/tests/mock_Django/signals/tests.py

3
mossplix

Django.db.models.signals.pyでModelSignalクラスを次のようにモックすることで、Djangoシグナルをモックできます。

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

それはより多くの行ですが、それもかなりうまく機能します:)

2
Hassek

小さなクラスで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)
2
Lyudmil Nenov

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)

これにより、すべてのレシーバーがモックに置き換えられます。たとえば、登録したもの、プラグイン可能なアプリが登録したもの、Django自体が登録したもの)です。post_saveでこれを使用して問題が発生しても、驚かないでください。速報。

受信機を調べて、実際にそれをモックしたいかどうかを判断することをお勧めします。

2
cerberos

あなたが言ったように、mock.patch('myapp.myfile._support_function')は正しいが、mock.patch('myapp.myfile.signal_handler_post_save_user')は間違っている。

理由は次のとおりです。

テストを開始すると、いくつかのファイルがシグナルの実現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
_
1
loveyzhou