pytest-mock icon indicating copy to clipboard operation
pytest-mock copied to clipboard

Stop Iteration Error

Open Lucasmiguelmac opened this issue 3 years ago • 3 comments

  • I have the following TestGenerateAaliDict class that contains 2 tests.
  • Each of the test contained in the class will pass if run individually (comment the others)
  • Whenever I run them together, regardless of how I arrange the order, the first test will pass and the second will throw a StopIteration error.
  • This StopIteration error I get is commonly triggered when a side effect is called more than len(side_effect).
  • It seems mocks are leaking from one test to another. Even though I tried with resetting mocks at the end of each test with reset_mock():
# Reset
children_mock.reset_mock()
aali_get_mock.reset_mock()

and with manual resets:

# Reset
children_mock.side_effect = mocker.DEFAULT
children_mock.return_value = mocker.DEFAULT
aali_get_mock.side_effect = mocker.DEFAULT
aali_get_mock.return_value = mocker.DEFAULT

Here are the test files:

from pytest_mock import MockerFixture
from model_bakery import baker
from django_mock_queries.query import MockSet, MockModel

from areas.utils.logic import generate_aali_dict
from areas.models import AALI


class TestGenerateAaliDict:

    aali_1 = baker.prepare(AALI, aalia=None, id=1)
    aali_2 = baker.prepare(AALI, aalia=aali_1, id=2, aalia_id=aali_1.id)
    aali_3 = baker.prepare(AALI, aalia=aali_2, id=3, aalia_id=aali_2.id)
    aali_4 = baker.prepare(AALI, aalia=aali_3, id=4, aalia_id=aali_3.id)
    aali_5 = baker.prepare(AALI, aalia=aali_4, id=5, aalia_id=aali_4.id)

    def test_aali_has_only_children(self, mocker: MockerFixture):
        # Mock
        children_mock = mocker.Mock()
        children_mock.return_value.return_value.count.side_effect = [1] * 4 + [0]
        mocker.patch(
            'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager',
            children_mock
        )
        aali_get_mock = mocker.Mock()
        aali_get_mock.get.side_effect = (self.aali_1, self.aali_2, self.aali_3, self.aali_4, self.aali_5)
        mocker.patch(
            'areas.models.AALI.objects',
            aali_get_mock
        )
        # Assert
        assert generate_aali_dict(1) == {
            "aali-1": 1,
            "aali-2": 2,
            "aali-3": 3,
            "aali-4": 4,
            "aali-5": 5,
        }
        
    def test_aali_has_only_parent(self, mocker: MockerFixture):
        # Mock
        aali_get_mock = mocker.Mock()
        aali_get_mock.get.return_value = self.aali_5
        mocker.patch(
            'areas.models.AALI.objects',
            aali_get_mock
        )
        children_mock = mocker.Mock()
        children_mock.return_value.return_value.count.return_value = 0
        mocker.patch(
            'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager',
            children_mock
        )
        # Assert
        assert generate_aali_dict(5) == {
            "aali-1": 1,
            "aali-2": 2,
            "aali-3": 3,
            "aali-4": 4,
            "aali-5": 5,
        }

    def test_aali_has_parent_and_children(self, mocker):
        # Mock
        children_mock = mocker.Mock()
        children_mock.return_value.return_value.count.side_effect = [1] * 2 + [0]
        mocker.patch(
            'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager',
            children_mock
        )
        aali_get_mock = mocker.Mock()
        aali_get_mock.get.side_effect = (self.aali_3, self.aali_4, self.aali_5)
        mocker.patch(
            'areas.models.AALI.objects',
            aali_get_mock
        )
        # Assert
        assert generate_aali_dict(3) == {
            "aali-1": 1,
            "aali-2": 2,
            "aali-3": 3,
            "aali-4": 4,
            "aali-5": 5,
        }

Here's the full traceback:

_____________________________________________________________________ TestGenerateAaliDict.test_aali_has_only_parent ______________________________________________________________________

self = <test_areas.test_utils.TestGenerateAaliDict object at 0x7fe559819db0>, mocker = <pytest_mock.plugin.MockerFixture object at 0x7fe559255150>

    def test_aali_has_only_parent(self, mocker: MockerFixture):
        # Mock
        aali_get_mock = mocker.Mock()
        aali_get_mock.get.return_value = self.aali_5
        mocker.patch(
            'areas.models.AALI.objects',
            aali_get_mock
        )
        children_mock = mocker.MagicMock()
        children_mock.return_value.return_value.count.return_value = 0
        mocker.patch(
            'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager',
            children_mock
        )
        # Assert
>       assert generate_aali_dict(5) == {
            "aali-1": 1,
            "aali-2": 2,
            "aali-3": 3,
            "aali-4": 4,
            "aali-5": 5,
        }

tests/test_areas/test_utils.py:56: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
areas/utils/logic.py:15: in generate_aali_dict
    while aali.children.count() > 0:
/usr/local/lib/python3.10/unittest/mock.py:1104: in __call__
    return self._mock_call(*args, **kwargs)
/usr/local/lib/python3.10/unittest/mock.py:1108: in _mock_call
    return self._execute_mock_call(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <MagicMock name='mock()().count' id='140623019748960'>, args = (), kwargs = {}, effect = <list_iterator object at 0x7fe559240d00>

    def _execute_mock_call(self, /, *args, **kwargs):
        # separate from _increment_mock_call so that awaited functions are
        # executed separately from their call, also AsyncMock overrides this method
    
        effect = self.side_effect
        if effect is not None:
            if _is_exception(effect):
                raise effect
            elif not _callable(effect):
>               result = next(effect)
E               StopIteration

/usr/local/lib/python3.10/unittest/mock.py:1165: StopIteration
==================================================================================== warnings summary =====================================================================================
../usr/local/lib/python3.10/site-packages/django/utils/version.py:6
  /usr/local/lib/python3.10/site-packages/django/utils/version.py:6: DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 632 for potential alternatives
    from distutils.version import LooseVersion

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================================================================= short test summary info =================================================================================
FAILED tests/test_areas/test_utils.py::TestGenerateAaliDict::test_aali_has_only_parent - StopIteration

I would love try to do this with context managers, but I don't know how to get away with using them for two nested mocks per test like I'm using right now.

Lucasmiguelmac avatar Apr 11 '22 21:04 Lucasmiguelmac

Hi @Lucasmiguelmac,

Whenever I run them together, regardless of how I arrange the order, the first test will pass and the second will throw a StopIteration error.

Didn't look at the example too deeply, but this suggests some global state is leaking from one test to the other. Glancing at your code, this instances are being shared between tests:

class TestGenerateAaliDict:

    aali_1 = baker.prepare(AALI, aalia=None, id=1)
    aali_2 = baker.prepare(AALI, aalia=aali_1, id=2, aalia_id=aali_1.id)
    aali_3 = baker.prepare(AALI, aalia=aali_2, id=3, aalia_id=aali_2.id)
    aali_4 = baker.prepare(AALI, aalia=aali_3, id=4, aalia_id=aali_3.id)
    aali_5 = baker.prepare(AALI, aalia=aali_4, id=5, aalia_id=aali_4.id)

I suggest to move them to an autouse fixture, so you get new fresh instances for each test:

class TestGenerateAaliDict:

    @pytest.fixture(autouse=True)
    def setup_aali(self):
        self.aali_1 = baker.prepare(AALI, aalia=None, id=1)
        self.aali_2 = baker.prepare(AALI, aalia=aali_1, id=2, aalia_id=aali_1.id)
        self.aali_3 = baker.prepare(AALI, aalia=aali_2, id=3, aalia_id=aali_2.id)
        self.aali_4 = baker.prepare(AALI, aalia=aali_3, id=4, aalia_id=aali_3.id)
        self.aali_5 = baker.prepare(AALI, aalia=aali_4, id=5, aalia_id=aali_4.id)

nicoddemus avatar Apr 13 '22 16:04 nicoddemus

Hi @nicoddemus. Unfotunately this did not change anything. The state that is indeed leaking, I suspect comes from the mock objects. Since this is an error that happens when a side_effect was called more times than expected.

What would fix this I think, is try to mock inside a context manager. But this mocks I'm using are rather complex in the sense they are really nested.

Lucasmiguelmac avatar Apr 15 '22 02:04 Lucasmiguelmac

Not sure what else to try. :/

However declaring the backer.prepare instances at the class level like it was being done is probably not what you want.

nicoddemus avatar Apr 22 '22 13:04 nicoddemus