Python-Unit-Test mit Basis- und Unterklasse

148

Ich habe derzeit einige Unit-Tests, die eine gemeinsame Reihe von Tests teilen. Hier ist ein Beispiel:

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

Die Ausgabe der oben genannten ist:

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

OK

Gibt es eine Möglichkeit, das Obige so umzuschreiben, dass das allererste testCommonnicht aufgerufen wird?

BEARBEITEN: Anstatt 5 Tests oben auszuführen, möchte ich, dass nur 4 Tests ausgeführt werden, 2 vom SubTest1 und weitere 2 vom SubTest2. Es scheint, dass Python unittest den ursprünglichen BaseTest alleine ausführt, und ich brauche einen Mechanismus, um dies zu verhindern.

Thierry Lam
quelle
Ich sehe, niemand hat es erwähnt, aber haben Sie die Möglichkeit, den Hauptteil zu ändern und eine Testsuite auszuführen, die alle Unterklassen von BaseTest enthält?
Kon Psych

Antworten:

154

Verwenden Sie die Mehrfachvererbung, damit Ihre Klasse mit allgemeinen Tests nicht selbst von TestCase erbt.

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()
Matthew Marshall
quelle
1
Das ist die bisher eleganteste Lösung.
Thierry Lam
27
Diese Methode funktioniert nur für setUp- und tearDown-Methoden, wenn Sie die Reihenfolge der Basisklassen umkehren. Da die Methoden in unittest.TestCase definiert sind und nicht super () aufrufen, müssen alle setUp- und tearDown-Methoden in CommonTests im MRO an erster Stelle stehen, sonst werden sie überhaupt nicht aufgerufen.
Ian Clelland
32
Nur um Ian Clellands Bemerkung zu verdeutlichen, damit es für Leute wie mich klarer wird: Wenn Sie der Klasse Methoden hinzufügen setUpund sie für jeden Test in abgeleiteten Klassen aufrufen möchten, müssen Sie die Reihenfolge der Basisklassen umkehren. so dass es sein wird : . tearDownCommonTestsclass SubTest1(CommonTests, unittest.TestCase)
Dennis Golomazov
6
Ich bin nicht wirklich ein Fan dieses Ansatzes. Dadurch wird ein Vertrag im Code erstellt, den Klassen von unittest.TestCase und erben müssen CommonTests. Ich denke, die folgende setUpClassMethode ist die beste und weniger anfällig für menschliches Versagen. Entweder das oder das Umschließen der BaseTest-Klasse in eine Containerklasse, die etwas hackiger ist, aber die Überspringmeldung im Ausdruck des Testlaufs vermeidet.
David Sanders
10
Das Problem bei diesem ist, dass Pylint eine Anpassung hat, weil CommonTestses Methoden aufruft, die in dieser Klasse nicht existieren.
MadScientist
145

Verwenden Sie keine Mehrfachvererbung, sie wird Sie später beißen .

Stattdessen können Sie Ihre Basisklasse einfach in das separate Modul verschieben oder mit der leeren Klasse umschließen:

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

Die Ausgabe:

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

OK
Vadim P.
quelle
6
Das ist mein Lieblings. Es ist das am wenigsten hackige Mittel und beeinträchtigt nicht das Überschreiben von Methoden, ändert die MRO nicht und ermöglicht mir, setUp, setUpClass usw. in der Basisklasse zu definieren.
Hannes
6
Ich verstehe es ernsthaft nicht (woher kommt die Magie?), Aber es ist meiner Meinung nach die beste Lösung :) Ich komme aus Java und hasse Mehrfachvererbung ...
Edouard Berthe
4
@Edouardb unittest führt nur Klassen auf Modulebene aus, die von TestCase erben. BaseTest ist jedoch nicht auf Modulebene.
JoshB
Als sehr ähnliche Alternative können Sie das ABC in einer No-Args-Funktion definieren, die das ABC zurückgibt, wenn es aufgerufen wird
Anakhand
34

Sie können dieses Problem mit einem einzigen Befehl lösen:

del(BaseTest)

Der Code würde also so aussehen:

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()
Wojciech B.
quelle
3
BaseTest ist ein Mitglied des Moduls, während es definiert wird, sodass es als Basisklasse der SubTests verwendet werden kann. Kurz bevor die Definition vollständig ist, entfernt del () sie als Mitglied, sodass das unittest Framework sie nicht findet, wenn es im Modul nach TestCase-Unterklassen sucht.
Mhsmith
3
Das ist eine großartige Antwort! Ich mag es mehr als das von @MatthewMarshall, weil in seiner Lösung Syntaxfehler von pylint angezeigt werden, da die self.assert*Methoden in einem Standardobjekt nicht vorhanden sind.
SimplyKnownAsG
1
Funktioniert nicht, wenn BaseTest an einer anderen Stelle in der Basisklasse oder ihren Unterklassen referenziert wird, z. B. wenn super () in Methodenüberschreibungen super( BaseTest, cls ).setUpClass( )
Hannes
1
@Hannes Zumindest in Python 3 BaseTestkann durch super(self.__class__, self)oder nur super()in den Unterklassen verwiesen werden , obwohl dies anscheinend nicht der Fall ist , wenn Sie Konstruktoren erben würden . Vielleicht gibt es auch eine solche "anonyme" Alternative, wenn die Basisklasse sich selbst referenzieren muss (nicht, dass ich eine Idee habe, wenn eine Klasse sich selbst referenzieren muss).
Stein
28

Die Antwort von Matthew Marshall ist großartig, erfordert jedoch, dass Sie in jedem Ihrer Testfälle von zwei Klassen erben, was fehleranfällig ist. Stattdessen benutze ich dies (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()
Dennis Golomazov
quelle
3
Das ist ordentlich. Gibt es eine Möglichkeit, einen Skip zu verwenden? Überspringen ist für mich unerwünscht und wird verwendet, um ein Problem im aktuellen Testplan anzuzeigen (entweder mit dem Code oder dem Test)?
Zach Young
@ZacharyYoung Ich weiß nicht, vielleicht können andere Antworten helfen.
Dennis Golomazov
@ZacharyYoung Ich habe versucht, dieses Problem zu beheben, siehe meine Antwort.
Simonzack
Es ist nicht sofort klar, was von Natur aus fehleranfällig ist, wenn man von zwei Klassen erbt
jwg
@jwg siehe Kommentare zur akzeptierten Antwort :) Sie müssen jede Ihrer Testklassen von den beiden Basisklassen erben; Sie müssen die richtige Reihenfolge von ihnen beibehalten; Wenn Sie eine weitere Basistestklasse hinzufügen möchten, müssen Sie auch davon erben. An Mixins ist nichts auszusetzen, aber in diesem Fall können sie durch ein einfaches Überspringen ersetzt werden.
Dennis Golomazov
7

Was versuchst du zu erreichen? Wenn Sie allgemeinen Testcode haben (Zusicherungen, Vorlagentests usw.), platzieren Sie diese in Methoden, denen kein Präfix vorangestellt ist, testdamit unittestsie nicht geladen werden.

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()
John Millikin
quelle
1
Würde common_assertion () nach Ihrem Vorschlag beim Testen der Unterklassen immer noch automatisch ausgeführt?
Stewart
@ Stewart Nein würde es nicht. Die Standardeinstellung ist, nur Methoden auszuführen, die mit "test" beginnen.
CS
6

Matthews Antwort ist die, die ich verwenden musste, da ich immer noch auf 2.5 bin. Ab 2.7 können Sie den Dekorator @ unittest.skip () für alle Testmethoden verwenden, die Sie überspringen möchten.

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

Sie müssen Ihren eigenen überspringenden Dekorateur implementieren, um den Basistyp zu überprüfen. Haben Sie diese Funktion nicht vor verwendet, aber aus der Spitze von meinem Kopf konnte man BaseTest als verwendet Marker Art den Sprung zu konditionieren:

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

Eine Möglichkeit, dies zu lösen, besteht darin, die Testmethoden auszublenden, wenn die Basisklasse verwendet wird. Auf diese Weise werden die Tests nicht übersprungen, sodass die Testergebnisse in vielen Testberichtstools grün statt gelb sein können.

Im Vergleich zur Mixin-Methode beklagen sich Ideen wie PyCharm nicht darüber, dass Unit-Test-Methoden in der Basisklasse fehlen.

Wenn eine Basisklasse von dieser Klasse erbt, muss sie die Methoden setUpClassund überschreiben 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 = []
Simonzack
quelle
5

Sie können die __test_ = FalseBaseTest-Klasse hinzufügen. Wenn Sie sie jedoch hinzufügen, müssen Sie __test__ = Trueabgeleitete Klassen hinzufügen , um Tests ausführen zu können.

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()
Peja
quelle
4

Eine andere Option ist nicht auszuführen

unittest.main()

Stattdessen können Sie verwenden

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

Sie führen also nur die Tests in der Klasse aus TestClass

Engel
quelle
Dies ist die am wenigsten hackige Lösung. Anstatt zu ändern, was unittest.main()in der Standardsuite erfasst wird, bilden Sie eine explizite Suite und führen deren Tests aus.
Zgoda
1

Ich habe ungefähr das Gleiche gemacht wie @Vladim P. ( https://stackoverflow.com/a/25695512/2451329 ), aber leicht modifiziert:

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

und los geht's.

gst
quelle
1

Ab Python 3.2 können Sie einem Modul eine test_loader- Funktion hinzufügen, um zu steuern, welche Tests (falls vorhanden) vom Testerkennungsmechanismus gefunden werden.

Im Folgenden werden beispielsweise nur die Originalposter SubTest1und SubTest2Testfälle geladen, wobei Folgendes ignoriert wird Base:

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

Es sollte möglich sein , iterieren standard_tests(a TestSuiteund kopieren Sie alle Tests der Standard - loader Enthalten gefunden) , aber Basezu suitestatt, aber die verschachtelte Natur TestSuite.__iter__macht , dass viel mehr kompliziert.

jbosch
quelle
0

Benennen Sie die testCommon-Methode einfach in etwas anderes um. Unittest überspringt (normalerweise) alles, was keinen "Test" enthält.

Schnell und einfach

  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()`
Kashif Siddiqui
quelle
2
Dies hätte zur Folge, dass der methodCommon-Test in keinem der SubTests ausgeführt wird.
Pfeffer Lebeck-Jobe
0

Das ist also eine Art alter Thread, aber ich bin heute auf dieses Problem gestoßen und habe mir meinen eigenen Hack dafür ausgedacht. Es wird ein Dekorator verwendet, der die Werte der Funktionen None festlegt, wenn er über die Basisklasse aufgerufen wird. Sie müssen sich keine Gedanken über Setup und Setup-Klasse machen, denn wenn die Basisklasse keine Tests enthält, werden sie nicht ausgeführt.

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()
Nathan Buckner
quelle
-2

Ändern Sie den Namen der BaseTest-Methode in 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)

Ausgabe:

Lief 2 Tests in 0,000s

Aufrufen von BaseTest: testCommon Aufrufen von
SubTest1: testSub1 Aufrufen von
BaseTest: testCommon Aufrufen von
SubTest2: testSub2

Aus der Dokumentation :

TestCase.setUp ()
Methode zur Vorbereitung der Testvorrichtung . Dies wird unmittelbar vor dem Aufruf der Testmethode aufgerufen. Jede durch diese Methode ausgelöste Ausnahme wird eher als Fehler als als Testfehler betrachtet. Die Standardimplementierung führt nichts aus.

Brian R. Bondy
quelle
Das würde funktionieren, was ist, wenn ich n testCommon habe, sollte ich sie alle unter platzieren setUp?
Thierry Lam
1
Ja, Sie sollten den gesamten Code, der kein tatsächlicher Testfall ist, unter setUp ablegen.
Brian R. Bondy
Wenn eine Unterklasse jedoch mehr als eine test...Methode hat, setUpwird sie immer wieder ausgeführt, einmal pro solcher Methode. Es ist also keine gute Idee, dort Tests durchzuführen!
Alex Martelli
Ich bin mir nicht sicher, was OP wollte, wenn es in einem komplexeren Szenario ausgeführt wird.
Brian R. Bondy