2016-05-27 7 views
5

So ist der Beispielcode ist sehr einfach:Mocking side_effect während autospeccing die Funktion zusätzliches Argument gibt

@mock.patch.object(BookForm, 'is_valid') 
def test_edit(self, mocked_is_valid): 
    create_superuser() 
    self.client.login(username="test", password="test") 

    book = Book() 
    book.save() 

    mocked_is_valid.side_effect = lambda: True 

    self.client.post(reverse('edit', args=[book.pk]), {}) 

Das funktioniert gut. Aber das Hinzufügen Autospec Schlüsselwort zum mock:

@mock.patch.object(BookForm, 'is_valid', autospec=True) 

zusätzliches Argument bewirkt, dass die side_effect aufrufbar weitergegeben werden, was offensichtlich ein Fehler aufgetreten ist:

Was ich uderstand nicht, weshalb autospeccing gibt zusätzliches Argument. Ich habe die docs gelesen, kann aber immer noch nicht die Erklärung für dieses Verhalten finden.

Theoretisch es geschrieben, dass

In addition mocked functions/methods have the same call signature as the original so they raise a TypeError if they are called incorrectly.

so würde es in Ordnung sein (is_valid hat self Argument, das wahrscheinlich ist, was hier übergeben wird), aber auf der anderen Seite ist es auch über side_effect dass

geschrieben

The function is called with the same arguments as the mock, and unless it returns DEFAULT, the return value of this function is used as the return value.

so soweit ich verstehe, side_effect sollte mit dem self Argumente auch ohne autospeccing aufgerufen werden. Aber es ist nicht.

is called with the same arguments as the mock

if form.is_valid(): # the mock is_valid is called with the self argument, isn't it? 

Also, wenn es mir jemand erklären könnte, vorzugsweise die Dokumentation zu zitieren, würde ich dankbar sein.

Antwort

0

Sie missverstehen die Dokumentation. Ohne autospec wird die side_effect, die aufgerufen wird, buchstäblich wie ohne die ursprüngliche Deklaration zu überprüfen ist. Lassen Sie uns ein besseres Minimalbeispiel erstellen, um dieses Problem zu demonstrieren.

class Book(object): 
    def __init__(self): 
     self.valid = False 
    def save(self): 
     self.pk = 'saved' 
    def make_valid(self): 
     self.valid = True 

class BookForm(object): 
    def __init__(self, book): 
     self.book = book 
    def is_valid(self): 
     return self.book.valid 

class Client(object): 
    def __init__(self, book): 
     self.form = BookForm(book) 
    def post(self): 
     if self.form.is_valid() is True: # to avoid sentinel value 
      print('Book is valid') 
     else: 
      print('Book is invalid') 

Jetzt ist Ihre ursprüngliche Test sollte

@mock.patch.object(BookForm, 'is_valid') 
def test_edit(mocked_is_valid): 
    book = Book() 
    book.save() 
    client = Client(book) 
    mocked_is_valid.side_effect = lambda: True 
    client.post() 

Durchführung des Tests mit einigen Anpassungen über die gleiche Arbeit wie verursachen ist Book is valid zu stdout gedruckt werden, auch wenn wir nicht durch den Tanz zu gegangen sind setze das Book.valid-Flag auf "true", da der self.form.is_valid, der in Client.post aufgerufen wird, durch das Lambda ersetzt wird, das aufgerufen wird. Wir können dies durch einen Debugger sehen:

> /usr/lib/python3.4/unittest/mock.py(962)_mock_call() 
-> ret_val = effect(*args, **kwargs) 
(Pdb) pp effect 
<function test_edit.<locals>.<lambda> at 0x7f021dee6730> 
(Pdb) bt 
... 
    /tmp/test.py(20)post() 
-> if self.form.is_valid(): 
    /usr/lib/python3.4/unittest/mock.py(896)__call__() 
-> return _mock_self._mock_call(*args, **kwargs) 
    /usr/lib/python3.4/unittest/mock.py(962)_mock_call() 
-> ret_val = effect(*args, **kwargs) 

Auch im Rahmen des Client.post Methodenaufruf, es ist nicht eine gebundene Methode (wir werden diese später zurück)

(Pdb) self.form.is_valid 
<MagicMock name='is_valid' id='140554947029032'> 

So hmm, wir könnte hier ein Problem haben: die side_effect könnte buchstäblich alle aufrufbar sein, die anders sein könnte, was Realität ist, in unserem Fall die is_valid Funktionssignatur (das ist die Argumentliste) könnte anders sein als die Mock, die wir bieten. Was passiert, wenn die BookForm.is_valid Verfahren modifiziert wurde, in einem zusätzlichen Argument zu nehmen:

class BookForm(object): 
    def __init__(self, book): 
     self.book = book 
    def is_valid(self, authcode): 
     return authcode > 0 and self.book.valid 

Rerun unserem Test ...und Sie werden sehen, dass unser Test hat bestanden, obwohl Client.post noch ruft BookForm.is_validohne Argumente. Ihr Produkt wird in der Produktion ausfallen, obwohl Ihr Test bestanden hat. Aus diesem Grund autospec Argument eingeführt wird, und wir werden das in unserem zweiten Test anzuwenden, ohne das aufrufbar durch side_effect ersetzt:

@mock.patch.object(BookForm, 'is_valid', autospec=True) 
def test_edit_autospec(mocked_is_valid): 
    book = Book() 
    book.save() 
    client = Client(book) 
    client.post() 

Dies geschieht nun beim Aufruf der Funktion

Traceback (most recent call last): 
    ... 
    File "/tmp/test.py", line 49, in test_edit_autospec 
    client.post() 
    File "/tmp/test.py", line 20, in post 
    if self.form.is_valid(): 
    ... 
    File "/usr/lib/python3.4/inspect.py", line 2571, in _bind 
    raise TypeError(msg) from None 
TypeError: 'authcode' parameter lacking default value 

Welche ist das, was Sie wollen und welche autospec beabsichtigt zu liefern - eine Prüfung vor der Mocks genannt werden, und

In addition mocked functions/methods have the same call signature as the original so they raise a TypeError if they are called incorrectly.

So haben wir diebehebenMethode mit einem Authcode größer als 0.

def post(self): 
     if self.form.is_valid(123) is True: 
      print('Book is valid') 
     else: 
      print('Book is invalid') 

Da unser Test die Methode die is_valid Funktion über die side_effect aufrufbar, werden am Ende Druck Book is invalid nicht verspotten hat.

Nun, wenn wir die side_effect zur Verfügung stellen möchten, wird es die gleiche Signatur

@mock.patch.object(BookForm, 'is_valid', autospec=True) 
def test_edit_autospec(mocked_is_valid): 
    book = Book() 
    book.save() 
    client = Client(book) 
    mocked_is_valid.side_effect = lambda self, authcode: True 
    client.post() 

Book is valid wird nun gedruckt werden wieder übereinstimmen müssen. durch den Debugger gehen, dass autospec ‚d und verspotteten is_valid Objekt innerhalb des Rahmens des Client.post Methodenaufruf ist

(Pdb) self.form.is_valid 
<bound method BookForm.is_valid of <__main__.BookForm object at 0x7fd57f43dc88>> 

Ah, irgendwie die Methodensignatur ein einfaches MagicMock Objekt nicht zu inspizieren (man erinnere sich der <MagicMock name='is_valid' id='140554947029032'> zuvor erwähnt) und ein richtig gebunden Methode, die die self Argument bedeutet, wird nun in die Mock übergeben, die Lösung dieses:

side_effect: A function to be called whenever the Mock is called. See the side_effect attribute. Useful for raising exceptions or dynamically changing return values. The function is called with the same arguments as the mock...

die „gleichen Argumente wie der Schein“ bedeutet in diesem Fall die gleiche wie was auch immer das in den Mock übergeben wurde. Um es zu wiederholen, der erste Fall der self.form.is_valid wurde durch eine nackte, unbegrenzte aufrufbar ersetzt, so dass self nie überschritten wird; und im zweiten Fall ist das Callable nun an self gebunden, sowohl die self als auch die authcode werden in die side_effect aufrufbar - genau so, was bei dem echten Anruf passieren würde. Dies sollte das wahrgenommene Fehlverhalten von Interaktionen mit autospec=True für mock.patch.object und manuell definierten side_effect für eine Scheinprobe in Einklang bringen.

Verwandte Themen