pythonunit-testingsubclasspython-unittest.mock

How to fool issubclass checks with a MagicMock?


I have something like this:

from unittest.mock import MagicMock

class A:
    pass

class B(A):
    pass

mock_B = MagicMock(spec_set=B)
assert issubclass(mock_B, A)  # TypeError: issubclass() arg 1 must be a class

How can I get this to pass?

isinstance and Mocking had a lot of stuff various answers, but from there I can't figure it out.


Solution

  • The problem here is that you want to treat mock_B, an instance of MagicMock, as a class, but an object in Python can only be considered as a class if it is an instance of type, which mock_B is simply not.

    So instead of trying to make a Mock object class-like, which is technically impossible since Python does not allow dynamically changing the type of an object, a workaround would be to make a class Mock-like. To do that, we can create a class with the same name and base classes as the class you want to mock, but then delegate all of its attribute lookups to a Mock object by making the __getattribute__ method of the class' metaclass perform such a delegation. This way, the new class passes the issubclass check easily because it is really a subclass of the intended base class A, and yet it also behaves like a Mock object because all attribute lookups are delegated to one.

    However, one problem with delegating all attribute lookups to a Mock object is that the resulting "class" would not behave like a class as the Mock object has no class-like attributes. We can solve this by defaulting attributes that aren't available in the Mock object to the class being mocked instead:

    from unittest.mock import MagicMock
    
    def mock_class(cls):
        class meta(type):
            def __getattribute__(self, name):
                try:
                    return getattr(mock, name)
                except AttributeError:
                    return getattr(cls, name)
    
        mock = MagicMock(spec_set=cls)
        return meta(cls.__name__, cls.__bases__, {})
    

    so that:

    class A:
        pass
    
    class B(A):
        def foo(self):
            pass
    
    mock_B = mock_class(B)
    assert issubclass(mock_B, A)
    print(mock_B)
    print(mock_B.foo)
    print(mock_B.__name__)
    print(mock_B.__bases__)
    

    passes the issubclass assertion and outputs:

    <class '__main__.B'>
    <MagicMock name='mock.foo' id='1970990059024'>
    B
    (<class '__main__.A'>,)
    

    Demo: Try it online!