interfaces - Python-Prueba de una clase base abstracta
como hacer una clase abstracta en python (6)
Estoy buscando formas / mejores prácticas en los métodos de prueba definidos en una clase base abstracta. Una cosa en la que puedo pensar directamente es realizar la prueba en todas las subclases concretas de la clase base, pero eso parece excesivo en algunos momentos.
Considera este ejemplo:
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"
¿Es posible probar la bar
sin hacer subclases?
Como lo puso adecuadamente el lunaryon, no es posible. El propósito mismo de los ABC que contienen métodos abstractos es que no son instanciables como se declaran.
Sin embargo, es posible crear una función de utilidad que inspecciona un ABC y crea una clase ficticia, no abstracta sobre la marcha. Esta función podría llamarse directamente dentro de su método / función de prueba y evitarle tener que escribir el código de la placa de caldera en el archivo de prueba solo para probar algunos métodos.
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)
En las versiones más recientes de Python puede usar unittest.mock.patch()
>>> import abc
>>> class A(metaclass = abc.ABCMeta):
... @abc.abstractmethod
... def foo(self): pass
Esto es lo que he encontrado: si establece que el atributo __abstractmethods__
sea un conjunto vacío, podrá instanciar una clase abstracta. Este comportamiento se especifica en PEP 3119 :
Si el conjunto
__abstractmethods__
resultante no está vacío, la clase se considera abstracta e intenta crear una instancia de TypeError.
Así que solo necesitas borrar este atributo durante la duración de las pruebas.
>>> A()
Traceback (most recent call last):
TypeError: Can''t instantiate abstract class A with abstract methods foo
No puede crear una instancia A:
>>> A.__abstractmethods__=set()
>>> A() #doctest: +ELLIPSIS
<....A object at 0x...>
Si anula __abstractmethods__
puede:
>>> 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
Funciona en ambos sentidos:
>>> 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
También puede usar unittest.mock
(desde 3.3) para anular temporalmente el comportamiento ABC.
class MyAbcClassTest(unittest.TestCase):
@patch.multiple(MyAbcClass, __abstractmethods__=set())
def test(self):
self.instance = MyAbcClass() # Ha!
No, no es. El propósito mismo de abc
es crear clases que no pueden ser instanciadas a menos que todos los atributos abstractos sean anulados con implementaciones concretas. Por lo tanto, debe derivar de la clase base abstracta y anular todos los métodos y propiedades abstractos.
Puede usar la práctica de herencia múltiple para tener acceso a los métodos implementados de la clase abstracta. Obviamente, seguir esa decisión de diseño depende de la estructura de la clase abstracta, ya que necesita implementar métodos abstractos (al menos traer la firma) en su caso de prueba.
Aquí está el ejemplo para su caso:
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)
Quizás una versión más compacta del concreto que propone @jsbueno podría ser:
def concreter(abclass):
class concreteCls(abclass):
pass
concreteCls.__abstractmethods__ = frozenset()
return type(''DummyConcrete'' + abclass.__name__, (concreteCls,), {})
La clase resultante aún tiene todos los métodos abstractos originales (que ahora se pueden llamar, incluso si es probable que esto no sea útil ...) y se pueden burlar según sea necesario.