How to create a mock that behaves like sub-classes from abstract class
Asked Answered
C

1

6

I'm trying to create mocks from scratch that can pass the test issubclass(class_mock, base_class) where the base class is an abstract class derived from abc.ABC. Before you ask the question, I will answer why I'm trying to do it.
I have an internal package containing a base class and a collection of sub-classes that properly implement the abstract interface. Besides, I have a factory class that can instantiate the sub-classes. The factory is built is such a way that it can inspect its own package and have access to the existing sub-classes. The factory is meant to be always in the same package as the derived and base class (constraint). I think you guessed that I'm actually testing the factory... However, since the sub-classes can change in number, their name or their package name, etc., I cannot implement a correct unit test that directly refers to the actual cub-classes (because it introduces a coupling) and I need mocks.
The problem is that I didn't succeed to create a mock that satisfies the above conditions for a class derived from an abstract class. What I was able to achieve is for a class derived from another non-abstract class.
Here is the code that illustrates the problem more concretely.

import unittest.mock
import inspect
import abc


class A:
    pass


class B(A):
    pass


class TestSubClass(unittest.TestCase):
    def test_sub_class(self):
        b_class_mock = self._create_class_mock("B", A)

        print(isinstance(b_class_mock, type))
        print(inspect.isclass(b_class_mock))
        print(issubclass(b_class_mock, A))

    @staticmethod
    def _create_class_mock(mock_name, base_class):
        class_mock = unittest.mock.MagicMock(spec=type(base_class), name=mock_name)
        class_mock.__bases__ = (base_class,)

        return class_mock

So, for this code, everything is ok. It prints 3 True as wanted.
But as long as the class A is defined as abstract (class A(abc.ABC)), the last test is failing with an error saying that the mock is not a class even if the 2 previous tests are saying the opposite.
I dived a bit into the implementation of abc.ABCMeta and found out that __subclasscheck__ is overridden. I tried to know the process behind it but when I reached the C code and everything became a way more complicated, I tried to rather track when the error message is generated. Unfortunately, I didn't succeed to understand why it is actually not working.

Commix answered 4/12, 2019 at 10:55 Comment(0)
W
1

This is because the implementation of the issubclass function of an abstract class has a hardcoded validation of the first argument that checks if the first argument has a type of type:

if (!PyType_Check(subclass)) {
    PyErr_SetString(PyExc_TypeError, "issubclass() arg 1 must be a class");
    return NULL;
}

So a mock object, despite implementing all the specs to quack like a class, still isn't considered a class by the issubclass implementation of an abstract class.

To satisfy issubclass's hardcoded validation, you can create a proxy class that quacks like a mock object instead:

def _create_class_mock(cls):
    class _MockABCMeta(abc.ABCMeta):
        def __getattribute__(self, name):
            try:
                return getattr(mock, name)
            except AttributeError:
                return getattr(cls, name)

    mock = MagicMock(spec=cls)
    return _MockABCMeta(cls.__name__, cls.__bases__, {})

so that:

class A(abc.ABC):
    pass

class B(A):
    pass

def test_sub_class():
    b_class_mock = _create_class_mock(B)

    print(isinstance(b_class_mock, type))
    print(inspect.isclass(b_class_mock))
    print(issubclass(b_class_mock, A))

test_sub_class()

outsputs:

True
True
True

Demo: https://ideone.com/IOmOY1

Wahoo answered 19/1 at 8:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.