2012-10-28 24 views
33

我有過一個裝飾連接signal_handler,這樣的事情很簡單:如何模擬django信號處理程序?

@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代替,一切正常。

回答

14

所以,我結束了一種-的解決方案:嘲笑的信號處理只是手段,模擬本身連接到信號,所以這恰恰是我所做的:

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 

請注意,在mock.patchautospec=True需要爲了使post_save.connect正確地在MagicMock工作,否則Django將提高一些例外,連接將失敗。

+1

的解決方案的原因不應該是'assert_equal'是' assertEquals(...)'? –

+2

這取決於您使用的測試套件; django默認使用'unittest',它具有'assertEquals';我總是使用「鼻子」,在我看來,鼻子在許多方面都很優越,而鼻子則帶有「assert_equal」。當寫我的答案時,我從我的生產代碼複製/粘貼,這就是爲什麼你在那裏看到'assert_equal'。我編輯了答案尊重django默認,謝謝你指出這個 – StefanoP

+0

謝謝!我也用鼻子,但從來沒有意識到assert_equal存在 –

2

有一種方法可以用小班模擬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) 
1

可以通過嘲笑ModelSignal類在django.db.models.signals.py嘲笑一個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() 

它更行,但它工作得很好太:)

9

可能是一個更好的想法是剔除內部的信號處理程序而不是處理程序本身。使用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) 
1

在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上使用此功能並且事情開始中斷,請不要驚訝。

您可能想檢查接收器以確定您是否真的想要嘲笑它。