49 votes

Python - Tester une classe de base abstraite

Je recherche des façons / meilleures pratiques pour tester les méthodes définies dans une classe de base abstraite. Une chose à laquelle je pense directement est d'effectuer le test sur toutes les sous-classes concrètes de la classe de base, mais cela semble parfois excessif.

Considérez cet exemple:

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"

Est-il possible de tester bar sans faire de sous-classement?

59voto

Secator Points 9827

Dans les nouvelles versions de Python, vous pouvez utiliser unittest.mock.patch()

class MyAbcClassTest(unittest.TestCase):

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

29voto

jb. Points 4932

Voici ce que j'ai trouvé : si vous définissez l'attribut __abstractmethods__ comme un ensemble vide, vous pourrez instancier une classe abstraite. Ce comportement est spécifié dans PEP 3119 :

Si l'ensemble __abstractmethods__ résultant n'est pas vide, la classe est considérée comme abstraite, et toute tentative de l'instancier lèvera une TypeError.

Vous avez simplement besoin de vider cet attribut pendant les tests.

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

Vous ne pouvez pas instancier A :

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

Si vous substituez __abstractmethods__ vous pouvez le faire :

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

Cela fonctionne dans les deux sens :

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

Vous pouvez également utiliser unittest.mock (à partir de 3.3) pour substituer temporairement le comportement 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

23voto

jsbueno Points 22212

Comme l'a dit lunaryon, ce n'est pas possible. Le but même des ABCs contenant des méthodes abstraites est qu'ils ne peuvent pas être instanciés tels quels.

Cependant, il est possible de créer une fonction utilitaire qui introspecte une ABC, et crée dynamiquement une classe factice, non abstraite. Cette fonction pourrait être appelée directement à l'intérieur de votre méthode/fonction de test et vous éviterait d'avoir à écrire du code inutile dans le fichier de test juste pour tester quelques méthodes.

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)

4voto

mehdi Points 74

Vous pouvez utiliser l'héritage multiple en pratique pour avoir accès aux méthodes implémentées de la classe abstraite. Évidemment, suivre une telle décision de conception dépend de la structure de la classe abstraite, car vous devez implémenter des méthodes abstraites (au moins apporter la signature) dans votre cas de test.

Voici l'exemple pour votre cas :

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)

4voto

lunaryorn Points 13621

Non, ce n'est pas. Le but même de abc est de créer des classes qui ne peuvent pas être instanciées à moins que toutes les attributs abstraits ne soient remplacés par des implémentations concrètes. Vous devez donc dériver de la classe de base abstraite et remplacer toutes les méthodes et propriétés abstraites.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X