web-dev-qa-db-ja.com

Python-抽象基本クラスのテスト

抽象基本クラスで定義されたメソッドをテストする方法/ベストプラクティスを探しています。私が直接考えることができる1つのことは、基本クラスのすべての具象サブクラスでテストを実行することですが、場合によっては過剰に見えることがあります。

この例を考えてみましょう:

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をテストすることは可能ですか?

39
bow

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)
21
jsbueno

新しいバージョンのPythonを使用できます unittest.mock.patch()

class MyAbcClassTest(unittest.TestCase):

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

これが私が見つけたものです:__abstractmethods__属性を空のセットに設定すると、抽象クラスをインスタンス化できるようになります。この動作は PEP 3119 で指定されています。

結果の__abstractmethods__セットが空でない場合、クラスは抽象と見なされ、インスタンス化しようとすると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
24
jb.

いいえ、ちがいます。 abcの目的は、すべての抽象属性が具体的な実装でオーバーライドされない限りインスタンス化できないクラスを作成することです。したがって、抽象基本クラスから派生し、すべての抽象メソッドとプロパティをオーバーライドする必要があります。

3
lunaryorn

おそらく、@ jsbuenoによって提案されたconcreterのよりコンパクトなバージョンは次のようになります。

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

結果のクラスには、元の抽象メソッドがすべて含まれており(これが役に立たないと思われる場合でも呼び出すことができます...)、必要に応じてモックすることができます。

2
splendido

多重継承 プラクティスを使用して、抽象クラスの実装されたメソッドにアクセスできます。テストケースで抽象メソッドを実装する(少なくともシグネチャをもたらす)必要があるため、このような設計決定に従うのは、抽象クラスの構造に依存します。

これがあなたのケースの例です:

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)
1
mehdi