2012-03-18 19 views
20

Poszukuję sposobów/najlepszych praktyk dotyczących metod testowania zdefiniowanych w abstrakcyjnej klasie bazowej. Jedną rzeczą, o której mogę myśleć bezpośrednio, jest wykonanie testu na wszystkich konkretnych podklasach klasy bazowej, ale czasami wydaje się to przesadne.Python - Testowanie abstrakcyjnej klasy bazowej

Rozważmy następujący przykład:

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" 

Czy to możliwe, aby przetestować bar bez jakiegokolwiek podklasy?

Odpowiedz

15

Właściwie ułożony przez lunaryon, nie jest możliwy. Celem ABC zawierających abstrakcyjne metody jest to, że nie są one natychmiastowe, jak zadeklarowano.

Możliwe jest jednak utworzenie funkcji użytecznej, która introspekcja ABC i tworzy w locie fikcyjną, nie abstrakcyjną klasę. Ta funkcja może zostać wywołana bezpośrednio w twojej metodzie/funkcji testu i zabezpieczyć cię przed koniecznością wymuszenia kodu płyty kotła na pliku testowym tylko w celu przetestowania kilku metod.

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. Spróbuję bawić się tym kodem w niektórych moich testach :). Dzięki! – bow

3

Nie, nie jest. Celem abc jest utworzenie klas, które nie mogą być tworzone, chyba że wszystkie abstrakcyjne atrybuty zostaną zastąpione konkretnymi implementacjami. W związku z tym należy czerpać z abstrakcyjnej klasy bazowej i zastępować wszystkie abstrakcyjne metody i właściwości.

+0

Hm..Jeśli tak, to czy mądrze byłoby zrobić fałszywą podklasę klasy abstrakcyjnej z abstrakcyjnymi metodami i właściwościami zdefiniowanymi, a następnie wykonać test na tym? – bow

+3

@bow: Tak, w ten sposób możesz to zrobić. – lunaryorn

15

Oto co znalazłem: Jeśli ustawisz atrybut __abstractmethods__ być zbiorem pustym będzie można utworzyć wystąpienia klasy abstrakcyjnej. Takie zachowanie jest określona w PEP 3119:

Jeśli uzyskany __abstractmethods__ zbiór jest niepusty, klasa jest uważany za streszczenie i próbuje oznacz ją podniesie TypeError.

Po prostu wyczyść ten atrybut na czas trwania testów.

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

Cant instancji:

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

Jeśli zastąpić __abstractmethods__ można:

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

To działa w obie strony:

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

Można również użyć unittest.mock (z 3.3), aby tymczasowo obejść zachowanie 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

W nowszych wersjach Pythona można użyć unittest.mock.patch()

class MyAbcClassTest(unittest.TestCase): 

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

Dlaczego naprawdę potrzebujemy "wielu"? –

+0

Możesz symulować wiele właściwości naraz za pomocą 'multiple()' –

+0

Tak, ale to nie usprawiedliwia jego użycia tutaj, ponieważ tylko jeden atrybut wydaje się załatany. Czy jest coś prostszego, co można wykorzystać? –

2

Może bardziej kompaktowa wersja betoniarz proponowanej przez @jsbueno mogą być:

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

Powstała klasa nadal ma wszystkie oryginalne metody abstrakcyjne (które można teraz nazwać, nawet jeśli nie jest to prawdopodobnie użyteczne ...) i można je wyśmiać jako n eeded.

Powiązane problemy