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
без каких-либо подклассов?
6 ответов
Как правильно сформулировано 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)
В новых версиях Python вы можете использовать unittest.mock.patch()
class MyAbcClassTest(unittest.TestCase):
@patch.multiple(MyAbcClass, __abstractmethods__=set())
def test(self):
self.instance = MyAbcClass() # Ha!
Вот что я нашел: если вы установите __abstractmethods__
атрибут будет пустым набором, вы сможете создать экземпляр абстрактного класса. Это поведение указано в PEP 3119:
Если в результате
__abstractmethods__
set не является пустым, класс считается абстрактным, и попытки создать его экземпляр вызовут 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
Возможно, более компактная версия бетонатора, предложенная @jsbueno, могла бы быть:
def concreter(abclass):
class concreteCls(abclass):
pass
concreteCls.__abstractmethods__ = frozenset()
return type('DummyConcrete' + abclass.__name__, (concreteCls,), {})
Результирующий класс по-прежнему имеет все оригинальные абстрактные методы (которые теперь можно вызывать, даже если это вряд ли будет полезно...) и может быть смоделирован при необходимости.
Вы можете использовать практику множественного наследования, чтобы иметь доступ к реализованным методам абстрактного класса. Очевидно, что следование такому проектному решению зависит от структуры абстрактного класса, поскольку вам необходимо реализовать абстрактные методы (по крайней мере, принести подпись) в ваш тестовый пример.
Вот пример для вашего случая:
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)
Нет, это не так. Сама цель abc
заключается в создании классов, которые не могут быть созданы, если все абстрактные атрибуты не переопределены конкретными реализациями. Следовательно, вам нужно наследовать от абстрактного базового класса и переопределять все абстрактные методы и свойства.