Подтвердить что ты не робот

Python - Тестирование абстрактного базового класса

Я ищу способы/рекомендации по методам тестирования, определенным в абстрактном базовом классе. Единственное, о чем я могу думать непосредственно, - это выполнить тест во всех конкретных подклассах базового класса, но это иногда кажется чрезмерным.

Рассмотрим следующий пример:

import abc

class Abstract(object):

    __metaclass__ = abc.ABCMeta

    @abc.abstractproperty
    def id(self):
        return   

    @abc.abstractmethod
    def foo(self):
        print "foo"

    def bar(self):
        print "bar"

Можно ли протестировать bar без какого-либо подкласса?

4b9b3361

Ответ 1

Как правильно положить lunaryon, это невозможно. Сама цель ABC, содержащая абстрактные методы, состоит в том, что они не являются инстанцируемыми, как объявлено.

Тем не менее, можно создать функцию утилиты, которая исследует ABC, и создает манекен, не абстрактный класс на лету. Эта функция может быть вызвана непосредственно внутри вашего тестового метода/функции и избавит вас от необходимости читать код плиты котла в тестовом файле только для тестирования нескольких методов.

def concreter(abclass):
    """
    >>> import abc
    >>> class Abstract(metaclass=abc.ABCMeta):
    ...     @abc.abstractmethod
    ...     def bar(self):
    ...        return None

    >>> c = concreter(Abstract)
    >>> c.__name__
    'dummy_concrete_Abstract'
    >>> c().bar() # doctest: +ELLIPSIS
    (<abc_utils.Abstract object at 0x...>, (), {})
    """
    if not "__abstractmethods__" in abclass.__dict__:
        return abclass
    new_dict = abclass.__dict__.copy()
    for abstractmethod in abclass.__abstractmethods__:
        #replace each abc method or property with an identity function:
        new_dict[abstractmethod] = lambda x, *args, **kw: (x, args, kw)
    #creates a new class, with the overriden ABCs:
    return type("dummy_concrete_%s" % abclass.__name__, (abclass,), new_dict)

Ответ 2

Вот что я нашел: если вы установите атрибут __abstractmethods__ как пустой набор, вы сможете создать экземпляр абстрактного класса. Это поведение указано в PEP 3119:

Если результирующий набор __abstractmethods__ не является пустым, класс считается абстрактным, и попытки его создания будут поднять TypeError.

Итак, вам просто нужно очистить этот атрибут на время тестирования.

>>> import abc
>>> class A(metaclass = abc.ABCMeta):
...     @abc.abstractmethod
...     def foo(self): pass

Вы не можете создать экземпляр A:

>>> A()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class A with abstract methods foo

Если вы переопределите __abstractmethods__, вы можете:

>>> A.__abstractmethods__=set()
>>> A() #doctest: +ELLIPSIS
<....A object at 0x...>

Он работает в обоих направлениях:

>>> class B(object): pass
>>> B() #doctest: +ELLIPSIS
<....B object at 0x...>

>>> B.__abstractmethods__={"foo"}
>>> B()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class B with abstract methods foo

Вы также можете использовать unittest.mock (от 3.3), чтобы временно переопределить поведение ABC.

>>> class A(metaclass = abc.ABCMeta):
...     @abc.abstractmethod
...     def foo(self): pass
>>> from unittest.mock import patch
>>> p = patch.multiple(A, __abstractmethods__=set())
>>> p.start()
{}
>>> A() #doctest: +ELLIPSIS
<....A object at 0x...>
>>> p.stop()
>>> A()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class A with abstract methods foo

Ответ 3

В более новых версиях Python вы можете использовать unittest.mock.patch()

class MyAbcClassTest(unittest.TestCase):

    @patch.multiple(MyAbcClass, __abstractmethods__=set())
    def test(self):
         self.instance = MyAbcClass() # Ha!

Ответ 4

Нет, нет. Сама цель abc заключается в создании классов, которые не могут быть созданы, если только все абстрактные атрибуты не переопределены конкретными реализациями. Следовательно, вам необходимо извлечь из абстрактного базового класса и переопределить все абстрактные методы и свойства.

Ответ 5

Возможно, более компактная версия конкретиста, предложенная @jsbueno, может быть:

def concreter(abclass):
    class concreteCls(abclass):
        pass
    concreteCls.__abstractmethods__ = frozenset()
    return type('DummyConcrete' + abclass.__name__, (concreteCls,), {})

Результирующий класс по-прежнему имеет все оригинальные абстрактные методы (которые теперь можно назвать, даже если это вряд ли будет полезно...), и их можно издеваться по мере необходимости.