Python - Тестирование абстрактного базового класса - PullRequest
26 голосов
/ 18 марта 2012

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

Рассмотрим этот пример:

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 без подклассов?

Ответы [ 6 ]

29 голосов
/ 26 февраля 2015

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

class MyAbcClassTest(unittest.TestCase):

    @patch.multiple(MyAbcClass, __abstractmethods__=set())
    def test(self):
         self.instance = MyAbcClass() # Ha!
19 голосов
/ 27 июня 2013

Вот что я нашел: если вы установите атрибут __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
18 голосов
/ 18 марта 2012

Как правильно сформулировано 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)
3 голосов
/ 18 марта 2012

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

2 голосов
/ 01 июня 2016

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

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

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

0 голосов
/ 04 сентября 2018

Вы можете использовать метод множественного наследования , чтобы получить доступ к реализованным методам абстрактного класса.Очевидно, что следование такому проектному решению зависит от структуры абстрактного класса, так как вам нужно реализовать абстрактные методы (по крайней мере, принести подпись) в вашем тестовом примере.

Вот пример для вашего случая:

class Abstract(object):

    __metaclass__ = abc.ABCMeta

    @abc.abstractproperty
    def id(self):
        return

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

    def bar(self):
        print("bar")

class AbstractTest(unittest.TestCase, Abstract):

    def foo(self):
        pass
    def test_bar(self):
        self.bar()
        self.assertTrue(1==1)
...