pythonmockingpython-unittest.mockmagicmock

MagicMock's reset_mock not properly resetting sub-mock's side_effect


I have a long-lived patch on a class, whose made instance undergoes multiple batches of assertions. Please see the below code snippet for the scenario.

It exposes (what I think is annoying) behavior in MagicMock.reset_mock where it seemingly creates a new MagicMock inside a sub-MagicMock:

from unittest.mock import MagicMock

mock_cls = MagicMock()
mock_cls.return_value.method.side_effect = [5]
instance = mock_cls()

# Batch 1 of usage: uses side_effect
assert instance.method() == 5

mock_cls.reset_mock(return_value=True, side_effect=True)
# After this, mock_cls.return_value.method has a new id

# Batch 2 of usage: uses return_value
instance.return_value.method.return_value = 6
assert instance.method() == 6  # StopIteration

When run with Python 3.10.2, it raises a StopIteration:

Traceback (most recent call last):
  File "/path/to/code/play/quick_play.py", line 9, in <module>
    assert instance.method() == 6
  File "/path/to/.pyenv/versions/3.10.2/lib/python3.10/unittest/mock.py", line 1104, in __call__
    return self._mock_call(*args, **kwargs)
  File "/path/to/.pyenv/versions/3.10.2/lib/python3.10/unittest/mock.py", line 1108, in _mock_call
    return self._execute_mock_call(*args, **kwargs)
  File "/path/to/.pyenv/versions/3.10.2/lib/python3.10/unittest/mock.py", line 1165, in _execute_mock_call
    result = next(effect)
StopIteration

Is it possible to use reset_mock without creating new MagicMock?

Alternately, how can I manually reset the side_effect so the snippet runs?


Aside

What is the visited=None argument for in reset_mock's signature? It's undocumented in the 3.10 docs, and here

def reset_mock(self,  visited=None,*, return_value=False, side_effect=False):

Solution

  • Sometimes in life, you answer your own question.

    Alternate to reset_mock

    From the side_effect docs

    If the function returns DEFAULT then the mock will return its normal value (from the return_value).

    from unittest.mock import MagicMock, DEFAULT
    
    mock_cls = MagicMock()
    mock_cls.return_value.method.side_effect = [5]
    instance = mock_cls()
    instance.method()
    
    # Works, aligns with docs
    mock_cls.return_value.method.side_effect = lambda: DEFAULT
    instance.method()  # <MagicMock name='mock().method()' ...>
    
    # Alternately, this works too
    mock_cls.return_value.method.side_effect = None
    instance.method()  # <MagicMock name='mock().method()' ...>
    

    You can also find this used here and mentioned here.


    Fixing My Snippet

    There are two learnings here:

    from unittest.mock import MagicMock
    
    mock_cls = MagicMock()
    mock_cls.return_value.method.side_effect = [5]
    instance = mock_cls()
    
    # Batch 1 of usage: uses side_effect
    assert instance.method() == 5
    
    mock_cls.return_value.method.side_effect = None
    # instance.reset_mock(side_effect=True)  # Also works
    
    # Batch 2 of usage: uses return_value
    instance.method.return_value = 6  # Correct
    # instance.return_value.method.return_value = 6  # Original incorrect
    assert instance.method() == 6
    

    Future readers: stay hungry, stay foolish.