Search code examples
pythonmockingpytestpython-unittestmonkeypatching

How to make a `unittest.mock._patch` instance subscriptable and iterable


I have a class which works approximately like this:

class Foo:

    def __init__(self, iterable: List[...]):
        self.iterable = iterable

    def __getitem__(self, i: int):
        return self.iterable[i]

    def __iter__(self):
        return iter(self.iterable)

So the indexing and iteration behavior operates on an attribute iterable (which is a list) without needing to explicitly reference that attribute. For example:

>>> foo = Foo(iterable=['a', 'b', 'c'])
>>> foo[-1]
'c'
>>> for x in foo:
...    print(x)
...
a
b
c

My current approach to testing this class uses unittest.mock.patch like this:

def test_foo():
    mock_foo = patch('my_module.Foo', autospec=True)
    mock_foo.iterable = ['a', 'b', 'c']
    mock_foo.__getitem__ = lambda self, i: self.iterable[i]
    mock_foo.__iter__ = lambda self: iter(self.iterable)

So I simply create the patch, then define the attributes and dunders necessary to replicate the behavior of the target class.

However, when I attempt to index into or iterate over the patch, it doesn't work:

>>> mock_foo[-1]
TypeError: '_patch' object is not subscriptable
>>> list(mock_project_export_set)
TypeError: '_patch' object is not iterable

This is unexpected and confusing, because when I invoke the dunders directly, everything works as desired:

>>> mock_foo.__getitem__(mock_foo, -1)
'c'
>>> list(mock_foo.__iter__(mock_foo))
['a', 'b', 'c']

Why the inconsistency? How can I replicate the behavior of my class in the test?

I'm not super experienced with patching/mocking, so if there's an altogether superior and simpler way, I'd love to know.

Python 3.11.3 My testing framework is pytest.


Solution

  • It looks like, you testing the patch instead of the MagicMock

    from the documentation of unittest.mock

    from unittest.mock import patch
    
    
    def test_foo():
        patcher = patch('my_module.foo.Foo', autospec=True) # depending on your structure
        mock_foo_class = patcher.start()
        mock_foo_class.iterable = ['a', 'b', 'c']
        mock_foo_class.__getitem__ = lambda self, i: self.iterable[i]
        mock_foo_class.__iter__ = lambda self: iter(self.iterable)
    

    An alternative approach would be using the patch as a decorator, depending on what u trying to accomplish:

    from unittest.mock import patch
    
    
    @patch('my_module.foo.Foo') # depending on your structure
    def test_foo(mock_foo):
        mock_foo.iterable = ['a', 'b', 'c']
        mock_foo.__getitem__ = lambda self, i: self.iterable[i]
        mock_foo.__iter__ = lambda self: iter(self.iterable)
        assert mock_foo[-1] == 'c'
        assert list(mock_foo) == mock_foo.iterable