So the example code is very basic:
@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]), {})
This works well. But adding autospec keyword to the mock:
@mock.patch.object(BookForm, 'is_valid', autospec=True)
causes additional argument to be passed to the side_effect
callable, which obviously results in error:
TypeError: <lambda>() takes 0 positional arguments but 1 was given
What I don't uderstand, is why autospeccing gives additional argument. I've read the docs, but still can't find the explanation of this behaviour.
Theoretically, it's written that
In addition mocked functions / methods have the same call signature as the original so they raise a TypeError if they are called incorrectly.
so it'd be okay (is_valid
has self
argument, which is probably what is being passed here), but on the other hand it's also written about side_effect
that
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 as far as I understand, side_effect
should be called with the self
argument even without autospeccing. But it is not.
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?
So if someone could explain it to me, preferably quoting the docs, I'd be thankful.
You are misunderstanding the documentation. Without autospec
, the side_effect
that is being called is literally as is without inspecting the original declaration. Let's create a better minimum example to demonstrate this issue.
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')
Now your original test should work about the same with some adjustments
@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()
Running the test as is will cause Book is valid
be printed to stdout, even though we haven't gone through the dance to set the Book.valid flag to true, as the self.form.is_valid
being called in Client.post
is replaced with the lambda which is invoked. We can see this through a debugger:
> /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)
Also within the frame of the Client.post
method call, it's not a bound method (we will get back to this later)
(Pdb) self.form.is_valid
<MagicMock name='is_valid' id='140554947029032'>
So hmm, we might have a problem here: the side_effect
could literally be any callable that could be different to what reality is, in our case the is_valid
function signature (that is the argument list) could be different to the mock we provide. What if the BookForm.is_valid
method was modified to take in an additional argument:
class BookForm(object):
def __init__(self, book):
self.book = book
def is_valid(self, authcode):
return authcode > 0 and self.book.valid
Rerun our test... and you will see that our test has passed, even though Client.post
is still calling BookForm.is_valid
without any arguments. Your product will fail in production even though your test has passed. This is why autospec
argument is introduced, and we will apply that in our second test without replacing the callable through side_effect:
@mock.patch.object(BookForm, 'is_valid', autospec=True)
def test_edit_autospec(mocked_is_valid):
book = Book()
book.save()
client = Client(book)
client.post()
This now happens when calling the function
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
Which is what you want and what autospec
intends to provide - a check before the mocks are called, and
In addition mocked functions / methods have the same call signature as the original so they raise a TypeError if they are called incorrectly.
So we have to fix the Client.post
method by providing an authcode greater than 0
.
def post(self):
if self.form.is_valid(123) is True:
print('Book is valid')
else:
print('Book is invalid')
Since our test didn't mock the is_valid
function via the side_effect
callable, the method will end up printing Book is invalid
.
Now if we want to provide the side_effect
, it will have to match the same signature
@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
will now be printed again. Going through the debugger to inspect that autospec
'd and mocked is_valid
object within the frame of the Client.post
method call
(Pdb) self.form.is_valid
<bound method BookForm.is_valid of <__main__.BookForm object at 0x7fd57f43dc88>>
Ah, somehow the method signature is not a simple MagicMock
object (recall the <MagicMock name='is_valid' id='140554947029032'>
noted previously) and is a properly bound method, which means the self
argument is now passed into the mock, solving this:
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...
The "same arguments as the mock" in this case means the same as whatever that was passed into the mock. To reiterate, the first case the self.form.is_valid
was replaced with a naked, unbounded callable so self
is never passed; and in the second case the callable is now bound to self
, both the self
AND authcode
will be passed into the side_effect
callable - just as what would happen in the real call. This should reconciled the perceived misbehavior of interactions with autospec=True
for mock.patch.object
and manually defined side_effect
callable for a mock.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With