Python – Prueba de una clase base abstracta

Estoy buscando formas / mejores prácticas en los métodos de prueba definidos en una clase base abstracta. Una cosa que se me ocurre 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 (, (), {}) """ 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()

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

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.

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

No puede crear una instancia A:

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

Si anula __abstractmethods__ puede:

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

Funciona en ambos sentidos:

 >>> 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 

También puede usar unittest.mock (desde 3.3) para anular temporalmente el comportamiento 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 

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.

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.

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)