私は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
をモックしますが、すべてが期待どおりに機能します。
だから、私は一種の解決策に行き着きました:シグナルハンドラーをモックすることは、単にモック自体をシグナルに接続することを意味するので、これはまさに私がしたことです:
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
をMagicMock
で正しく機能させるには、mock.patch
のpost_save.connect
が必要であることに注意してください。そうでない場合、Django例外が発生すると、接続は失敗します。
おそらく、より良いアイデアは、機能をモックアウトすることです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)
mock_Djangoを見てください。シグナルをサポートしています
https://github.com/dcramer/mock-Django/blob/master/tests/mock_Django/signals/tests.py
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()
それはより多くの行ですが、それもかなりうまく機能します:)
小さなクラスで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)
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
でこれを使用して問題が発生しても、驚かないでください。速報。
受信機を調べて、実際にそれをモックしたいかどうかを判断することをお勧めします。
あなたが言ったように、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
_