2012-03-18 16 views
20

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 algunas veces.Python - Prueba de una clase base abstracta

Considere 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 bar sin hacer ningún tipo de subclases?

Respuesta

15

Como correctamente indicado por lunaryon, no es posible. El mismo propósito del ABC que contiene métodos abstractos es que no son instanciables como se declara.

Sin embargo, es posible crear una función de utilidad que introspecta un ABC y crea una clase ficticia y no abstracta sobre la marcha. Esta función se puede invocar directamente dentro de su método/función de prueba y le evita tener que anotar el código de la placa de la 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) 
+0

Cool. Trataré de manipular este código en algunas de mis pruebas :). ¡Gracias! – bow

3

No, no lo es. El objetivo de abc es crear clases que no se puedan crear instancias a menos que todos los atributos abstractos se anulen con implementaciones concretas. Por lo tanto, debe derivar de la clase base abstracta y anular todos los métodos y propiedades abstractos.

+0

Hm..if eso es así, ¿sería prudente hacer una subclase falsa de la clase abstracta con los métodos abstractos y propiedades definidas, y luego hacer la prueba en él? – bow

+3

@bow: Sí, así es como harías esto. – lunaryorn

15

Aquí es lo que he encontrado: Si establece __abstractmethods__ el atributo de ser un conjunto vacío podrás crear una instancia de clase abstracta. Este comportamiento se especifica en PEP 3119:

Si el __abstractmethods__ conjunto resultante es no vacío, la clase se considera abstracta, y los intentos de crear una instancia que elevará TypeError.

Así que solo tiene que borrar este atributo mientras duren las pruebas.

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

Usted no puede crear una instancia:

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

Si reemplaza __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 utilizar 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 
14

En las nuevas versiones de Python puede utilizar unittest.mock.patch()

class MyAbcClassTest(unittest.TestCase): 

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

¿Por qué realmente necesitamos 'multiple'? –

+0

Puede burlarse múltiples propiedades a la vez con 'múltiple()' –

+0

Sí, pero eso no parecen justificar su uso aquí, ya que sólo un atributo parece parcheado. ¿Hay algo más simple que pueda usarse? –

2

Tal vez una versión más compacta del concreter propuesto por @jsbueno podrían ser:

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

La clase resultante todavía tiene todos los métodos abstractos originales (que ahora se pueden llamar, incluso si esto no es probable que sea útil ...) y se puede burlar como n eeded

Cuestiones relacionadas