Test jednostkowy Pythona z klasą podstawową i podrzędną


148

Obecnie mam kilka testów jednostkowych, które mają wspólny zestaw testów. Oto przykład:

import unittest

class BaseTest(unittest.TestCase):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

Wynik powyższego to:

Calling BaseTest:testCommon
.Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

Czy istnieje sposób na przepisanie powyższego, aby testCommonnie było wywoływane pierwsze ?

EDYCJA: Zamiast uruchamiać 5 testów powyżej, chcę, aby uruchamiał tylko 4 testy, 2 z SubTest1 i kolejne 2 z SubTest2. Wygląda na to, że Python unittest samodzielnie uruchamia oryginalny BaseTest i potrzebuję mechanizmu, aby temu zapobiec.


Widzę, że nikt o tym nie wspomniał, ale czy masz możliwość zmiany głównej części i uruchomienia zestawu testów, który ma wszystkie podklasy BaseTest?
kon psych,

Odpowiedzi:


154

Użyj dziedziczenia wielokrotnego, aby Twoja klasa z typowymi testami sama nie dziedziczyła po TestCase.

import unittest

class CommonTests(object):
    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(unittest.TestCase, CommonTests):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(unittest.TestCase, CommonTests):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

1
To jak dotąd najbardziej eleganckie rozwiązanie.
Thierry Lam

27
Ta metoda działa tylko w przypadku metod setUp i tearDown, jeśli odwrócisz kolejność klas bazowych. Ponieważ metody są zdefiniowane w unittest.TestCase i nie wywołują super (), wszelkie metody setUp i tearDown w CommonTests muszą być pierwsze w MRO, w przeciwnym razie nie zostaną wywołane.
Ian Clelland,

32
Aby wyjaśnić uwagę Iana Clellanda, aby była jaśniejsza dla ludzi takich jak ja: jeśli dodajesz setUpi tearDownmetody do CommonTestsklasy i chcesz, aby były wywoływane dla każdego testu w klasach pochodnych, musisz odwrócić kolejność klas bazowych, tak, że będzie: class SubTest1(CommonTests, unittest.TestCase).
Dennis Golomazov

6
Nie jestem fanem tego podejścia. To ustanawia kontrakt w kodzie, który klasy muszą dziedziczyć z obu unittest.TestCase i CommonTests . Myślę, że setUpClassponiższa metoda jest najlepsza i jest mniej podatna na błędy ludzkie. Albo to, albo zawinięcie klasy BaseTest w klasę kontenera, która jest nieco bardziej zmyślona, ​​ale pozwala uniknąć komunikatu o pomijaniu na wydruku przebiegu testowego.
David Sanders,

10
Problem z tym jest taki, że pylint ma dopasowanie, ponieważ wywołuje CommonTestsmetody, które nie istnieją w tej klasie.
MadScientist,

145

Nie używaj dziedziczenia wielokrotnego, bo ugryzie Cię to później .

Zamiast tego możesz po prostu przenieść swoją klasę bazową do oddzielnego modułu lub opakować ją pustą klasą:

class BaseTestCases:

    class BaseTest(unittest.TestCase):

        def testCommon(self):
            print('Calling BaseTest:testCommon')
            value = 5
            self.assertEqual(value, 5)


class SubTest1(BaseTestCases.BaseTest):

    def testSub1(self):
        print('Calling SubTest1:testSub1')
        sub = 3
        self.assertEqual(sub, 3)


class SubTest2(BaseTestCases.BaseTest):

    def testSub2(self):
        print('Calling SubTest2:testSub2')
        sub = 4
        self.assertEqual(sub, 4)

if __name__ == '__main__':
    unittest.main()

Wyjście:

Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

6
To jest mój ulubiony. Jest to najmniej hakerski sposób i nie koliduje z nadpisywaniem metod, nie zmienia MRO i pozwala mi zdefiniować setUp, setUpClass itp. W klasie bazowej.
Hannes

6
Naprawdę tego nie rozumiem (skąd bierze się magia?), Ale według mnie jest to zdecydowanie najlepsze rozwiązanie :) Pochodzę z Javy, nienawidzę wielokrotnego dziedziczenia ...
Edouard Berthe

4
@Edouardb unittest uruchamia tylko klasy na poziomie modułu, które dziedziczą po TestCase. Ale BaseTest nie jest na poziomie modułu.
JoshB

Jako bardzo podobną alternatywę możesz zdefiniować ABC wewnątrz funkcji no-args, która zwraca ABC po wywołaniu
Anakhand

34

Możesz rozwiązać ten problem za pomocą jednego polecenia:

del(BaseTest)

Więc kod wyglądałby tak:

import unittest

class BaseTest(unittest.TestCase):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

del(BaseTest)

if __name__ == '__main__':
    unittest.main()

3
BaseTest jest członkiem modułu podczas jego definiowania, więc jest dostępny do użycia jako klasa bazowa SubTests. Tuż przed zakończeniem definicji del () usuwa ją jako element członkowski, więc unittest framework nie znajdzie go podczas wyszukiwania podklas TestCase w module.
mhsmith

3
to jest niesamowita odpowiedź! Podoba mi się bardziej niż @MatthewMarshall, ponieważ w jego rozwiązaniu otrzymasz błędy składniowe z pylint, ponieważ self.assert*metody nie istnieją w standardowym obiekcie.
SimplyKnownAsG

1
Nie działa, jeśli BaseTest jest przywoływany gdziekolwiek indziej w klasie bazowej lub jej podklasach, np. Podczas wywoływania super () w nadpisaniach metody: super( BaseTest, cls ).setUpClass( )
Hannes

1
@Hannes Przynajmniej w Pythonie 3 BaseTestmożna odwoływać się przez podklasy super(self.__class__, self)lub tylko super()w podklasach, chociaż najwyraźniej nie, jeśli masz dziedziczyć konstruktory . Może jest też taka „anonimowa” alternatywa, gdy klasa bazowa musi się odwoływać (nie mam pojęcia, kiedy klasa musi odwoływać się do siebie).
Stein

28

Odpowiedź Matthew Marshalla jest świetna, ale wymaga dziedziczenia z dwóch klas w każdym z przypadków testowych, co jest podatne na błędy. Zamiast tego używam tego (python> = 2.7):

class BaseTest(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        if cls is BaseTest:
            raise unittest.SkipTest("Skip BaseTest tests, it's a base class")
        super(BaseTest, cls).setUpClass()

3
To fajne. Czy jest sposób, aby obejść konieczność korzystania z skipu? Dla mnie pominięcia są niepożądane i służą do wskazania problemu w bieżącym planie testów (z kodem lub testem)?
Zach Young,

@ZacharyYoung Nie wiem, może inne odpowiedzi mogą pomóc.
Dennis Golomazov

@ZacharyYoung Próbowałem rozwiązać ten problem, zobacz moją odpowiedź.
simonzack

nie jest od razu jasne, co jest z natury podatne na błędy w dziedziczeniu z dwóch klas
jwg

@jwg zobacz komentarze do zaakceptowanej odpowiedzi :) Musisz odziedziczyć każdą z klas testowych z dwóch klas bazowych; musisz zachować ich właściwą kolejność; jeśli chcesz dodać kolejną podstawową klasę testową, musisz ją również dziedziczyć. Nie ma nic złego w mieszankach, ale w tym przypadku można je zastąpić prostym pominięciem.
Dennis Golomazov

7

Co próbujesz osiągnąć? Jeśli masz wspólny kod testowy (potwierdzenia, testy szablonów itp.), Umieść je w metodach, które nie są poprzedzone prefiksem, testwięc unittestich nie załaduje.

import unittest

class CommonTests(unittest.TestCase):
      def common_assertion(self, foo, bar, baz):
          # whatever common code
          self.assertEqual(foo(bar), baz)

class BaseTest(CommonTests):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(CommonTests):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)

class SubTest2(CommonTests):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

1
Czy zgodnie z twoją sugestią metoda common_assertion () nadal będzie uruchamiana automatycznie podczas testowania podklas?
Stewart

@Stewart Nie, nie. Domyślnym ustawieniem jest uruchamianie tylko metod zaczynających się od „test”.
CS

6

Odpowiedź Matthew to ta, której potrzebowałem, ponieważ wciąż jestem na 2.5. Ale od wersji 2.7 możesz używać dekoratora @ unittest.skip () na dowolnych metodach testowych, które chcesz pominąć.

http://docs.python.org/library/unittest.html#skipping-tests-and-expected-failures

Aby sprawdzić typ podstawowy, musisz zaimplementować własny dekorator pomijający. Nie korzystałem wcześniej z tej funkcji, ale na myśl możesz użyć BaseTest jako typu markera do warunkowania przeskoku:

def skipBaseTest(obj):
    if type(obj) is BaseTest:
        return unittest.skip("BaseTest tests skipped")
    return lambda func: func

5

Sposób, w jaki myślałem o rozwiązaniu tego problemu, polega na ukryciu metod testowych, jeśli używana jest klasa bazowa. Dzięki temu testy nie są pomijane, więc wyniki testów mogą być zielone zamiast żółtego w wielu narzędziach do raportowania testów.

W porównaniu z metodą mixin, idee, takie jak PyCharm, nie będą narzekać, że w klasie bazowej brakuje metod testów jednostkowych.

Jeśli klasa bazowa dziedziczy po tej klasie, będzie musiała zastąpić metody setUpClassi tearDownClass.

class BaseTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls._test_methods = []
        if cls is BaseTest:
            for name in dir(cls):
                if name.startswith('test') and callable(getattr(cls, name)):
                    cls._test_methods.append((name, getattr(cls, name)))
                    setattr(cls, name, lambda self: None)

    @classmethod
    def tearDownClass(cls):
        if cls is BaseTest:
            for name, method in cls._test_methods:
                setattr(cls, name, method)
            cls._test_methods = []

5

Możesz dodać __test_ = Falseklasę BaseTest, ale jeśli ją dodasz, pamiętaj, że musisz dodać __test__ = Trueklasy pochodne, aby móc uruchamiać testy.

import unittest

class BaseTest(unittest.TestCase):
    __test__ = False

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):
    __test__ = True

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):
    __test__ = True

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

4

Inną opcją jest niewykonanie

unittest.main()

Zamiast tego możesz użyć

suite = unittest.TestLoader().loadTestsFromTestCase(TestClass)
unittest.TextTestRunner(verbosity=2).run(suite)

Więc wykonujesz testy tylko w klasie TestClass


To najmniej hakerskie rozwiązanie. Zamiast modyfikować to, co jest unittest.main()zbierane w domyślnym zestawie, tworzysz jawny zestaw i uruchamiasz jego testy.
zgoda

1

Zrobiłem mniej więcej to samo co @Vladim P. ( https://stackoverflow.com/a/25695512/2451329 ), ale nieco zmodyfikowałem:

import unittest2


from some_module import func1, func2


def make_base_class(func):

    class Base(unittest2.TestCase):

        def test_common1(self):
            print("in test_common1")
            self.assertTrue(func())

        def test_common2(self):
            print("in test_common1")
            self.assertFalse(func(42))

    return Base



class A(make_base_class(func1)):
    pass


class B(make_base_class(func2)):

    def test_func2_with_no_arg_return_bar(self):
        self.assertEqual("bar", func2())

i gotowe.


1

Począwszy od Pythona 3.2, możesz dodać test_loader do modułu, aby kontrolować, które testy (jeśli istnieją) są wyszukiwane przez mechanizm wykrywania testów.

Na przykład poniższe załadują tylko oryginalny plakat SubTest1i SubTest2przypadki testowe, ignorując Base:

def load_tests(loader, standard_tests, pattern):
    suite = TestSuite()
    suite.addTests([SubTest1, SubTest2])
    return suite

To powinno być możliwe do iteracyjnego standard_tests(a TestSuitezawierający testy ładowarka domyślny znalezione) i skopiować wszystko, ale Baseaby suitezamiast, ale zagnieżdżony charakter TestSuite.__iter__powoduje, że dużo bardziej skomplikowane.


0

Po prostu zmień nazwę metody testCommon na coś innego. Unittest (zwykle) pomija wszystko, co nie zawiera słowa „test”.

Szybko i prosto

  import unittest

  class BaseTest(unittest.TestCase):

   def methodCommon(self):
       print 'Calling BaseTest:testCommon'
       value = 5
       self.assertEquals(value, 5)

  class SubTest1(BaseTest):

      def testSub1(self):
          print 'Calling SubTest1:testSub1'
          sub = 3
          self.assertEquals(sub, 3)


  class SubTest2(BaseTest):

      def testSub2(self):
          print 'Calling SubTest2:testSub2'
          sub = 4
          self.assertEquals(sub, 4)

  if __name__ == '__main__':
      unittest.main()`

2
Mogłoby to spowodować niewykonanie testu methodCommon w żadnym z testów podrzędnych.
Pepper Lebeck-Jobe

0

To trochę stary wątek, ale dzisiaj natknąłem się na ten problem i pomyślałem o swoim własnym hacku. Używa dekoratora, który nadaje wartości funkcji None, gdy są dostępne za pośrednictwem klasy bazowej. Nie musisz martwić się konfiguracją i klasą konfiguracji, ponieważ jeśli klasa podstawowa nie ma testów, nie zostaną one uruchomione.

import types
import unittest


class FunctionValueOverride(object):
    def __init__(self, cls, default, override=None):
        self.cls = cls
        self.default = default
        self.override = override

    def __get__(self, obj, klass):
        if klass == self.cls:
            return self.override
        else:
            if obj:
                return types.MethodType(self.default, obj)
            else:
                return self.default


def fixture(cls):
    for t in vars(cls):
        if not callable(getattr(cls, t)) or t[:4] != "test":
            continue
        setattr(cls, t, FunctionValueOverride(cls, getattr(cls, t)))
    return cls


@fixture
class BaseTest(unittest.TestCase):
    def testCommon(self):
        print('Calling BaseTest:testCommon')
        value = 5
        self.assertEqual(value, 5)


class SubTest1(BaseTest):
    def testSub1(self):
        print('Calling SubTest1:testSub1')
        sub = 3
        self.assertEqual(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print('Calling SubTest2:testSub2')
        sub = 4
        self.assertEqual(sub, 4)

if __name__ == '__main__':
    unittest.main()

-2

Zmień nazwę metody BaseTest na setUp:

class BaseTest(unittest.TestCase):
    def setUp(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)


class SubTest1(BaseTest):
    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):
    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

Wynik:

Przeprowadziłem 2 testy w 0,000s

Wywołanie BaseTest: testCommon Calling
SubTest1: testSub1 Calling
BaseTest: testCommon Calling
SubTest2: testSub2

Z dokumentacji :

TestCase.setUp ()
Metoda wywołana w celu przygotowania osprzętu testowego. Jest to wywoływane bezpośrednio przed wywołaniem metody testowej; każdy wyjątek zgłoszony przez tę metodę będzie traktowany jako błąd, a nie niepowodzenie testu. Domyślna implementacja nic nie robi.


To by zadziałało, a co jeśli mam n testCommon, czy powinienem umieścić je wszystkie pod setUp?
Thierry Lam

1
Tak, powinieneś umieścić cały kod, który nie jest faktycznym przypadkiem testowym, w setUp.
Brian R. Bondy

Ale jeśli podklasa ma więcej niż jedną test...metodę, setUpjest wykonywana w kółko, raz na taką metodę; więc nie jest dobrym pomysłem umieszczanie tam testów!
Alex Martelli

Nie jestem pewien, czego chciał OP w przypadku wykonania w bardziej złożonym scenariuszu.
Brian R. Bondy
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.