Reputation: 106618
For the purpose of a unit test, I made a class whose instance is an iterable that would yield a certain sequence and then raise an exception:
class Iter:
def __init__(self, seq):
self.seq = seq
self.pos = 0
def __next__(self):
if self.pos == len(self.seq):
raise Exception
value = self.seq[self.pos]
self.pos += 1
return value
def __iter__(self):
return self
so that:
for value in Iter((1, 2, 3)):
print(value)
would output:
1
2
3
Traceback (most recent call last):
File "test.py", line 25, in <module>
for value in mocked_iterable:
File "test.py", line 11, in __next__
raise Exception
Exception
But why reinvent the wheel when MagicMock
already has a side_effect
attribute that should do the same? Per the documentation, the side_effect
attribute can be an iterable that yields either a value to be returned from the call to the mock, or an exception to raise, so it suits the purpose of mimicking the aforementioned class perfectly. I therefore created a MagicMock
object and made its __iter__
method return the object itself, and made its __next__
method to have a side effect of the desired sequence and the exception:
from unittest.mock import MagicMock
mocked_iterable = MagicMock()
mocked_iterable.__iter__.return_value = mocked_iterable
mocked_iterable.__next__.side_effect = [1, 2, 3, Exception]
for value in mocked_iterable:
print(value)
However, this outputs:
...
File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 1005, in _mock_call
ret_val = effect(*args, **kwargs)
File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 1793, in __iter__
return iter(ret_val)
File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 939, in __call__
return _mock_self._mock_call(*args, **kwargs)
File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 944, in _mock_call
self.called = True
RecursionError: maximum recursion depth exceeded
But the question is, why is there any recursion?
I found that I can work around this "bug" by putting the self reference in __iter__
's side_effect
attribute instead:
mocked_iterable = MagicMock()
mocked_iterable.__iter__.side_effect = [mocked_iterable]
mocked_iterable.__next__.side_effect = [1, 2, 3, Exception]
for value in mocked_iterable:
print(value)
This correctly outputs:
1
2
3
Traceback (most recent call last):
File "test.py", line 6, in <module>
for value in mocked_iterable:
File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 939, in __call__
return _mock_self._mock_call(*args, **kwargs)
File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 1000, in _mock_call
raise result
Exception
But is the recursion error indeed a bug, or a feature of mock
with an unintended consequence?
Upvotes: 3
Views: 875
Reputation: 20224
I agree that this is indeed a bug. Although this is an edge case.
As we can see in the source code. mock
module expects that iter(ret_val)
will return the unchanged iterator if ret_val
has already been an iterator.
Well, it actually does but still needs to call ret_val
's __iter__
method.
Upvotes: 2