Test unitario Python con base e sottoclasse


149

Al momento ho alcuni test unitari che condividono un set comune di test. Ecco un esempio:

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

L'output di quanto sopra è:

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

OK

C'è un modo per riscrivere quanto sopra in modo che il primo testCommonnon venga chiamato?

EDIT: Invece di eseguire 5 test sopra, voglio che esegua solo 4 test, 2 da SubTest1 e altri 2 da SubTest2. Sembra che Python unittest stia eseguendo il BaseTest originale da solo e ho bisogno di un meccanismo per impedire che ciò accada.


Vedo che nessuno lo ha menzionato, ma hai la possibilità di cambiare la parte principale ed eseguire una suite di test che ha tutte le sottoclassi di BaseTest?
kon psych,

Risposte:


154

Usa l'ereditarietà multipla, quindi la tua classe con test comuni non eredita di per sé da 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
Questa è la soluzione più elegante finora.
Thierry Lam,

27
Questo metodo funziona solo con i metodi setUp e tearDown se si inverte l'ordine delle classi base. Poiché i metodi sono definiti in unittest.TestCase e non chiamano super (), tutti i metodi setUp e tearDown in CommonTest devono essere i primi nell'MRO, altrimenti non verranno chiamati affatto.
Ian Clelland,

32
Solo per chiarire l'osservazione di Ian Clelland in modo che sia più chiaro per le persone come me: se aggiungi setUpe tearDownmetodi in CommonTestsclasse e vuoi che vengano chiamati per ogni test in classi derivate, devi invertire l'ordine delle classi di base, in modo che sia: class SubTest1(CommonTests, unittest.TestCase).
Dennis Golomazov,

6
Non sono un fan di questo approccio. Ciò stabilisce un contratto nel codice che le classi devono ereditare da entrambi unittest.TestCase e CommonTests . Penso che il setUpClassmetodo di seguito sia il migliore ed è meno soggetto all'errore umano. O che avvolge la classe BaseTest in una classe contenitore che è un po 'più caotica ma evita il messaggio di salto nella stampa dell'esecuzione del test.
David Sanders,

10
Il problema con questo è che il pylint ha una risposta perché CommonTestssta invocando metodi che non esistono in quella classe.
MadScientist,

146

Non utilizzare l'ereditarietà multipla, ti morderà in seguito .

Invece puoi semplicemente spostare la tua classe base nel modulo separato o avvolgerla con la classe vuota:

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

Il risultato:

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

OK

6
Questo è il mio preferito. È il mezzo meno complicato e non interferisce con i metodi di override, non altera l'MRO e mi permette di definire setUp, setUpClass ecc. Nella classe base.
Hannes,

6
Sul serio non capisco (da dove viene la magia?), Ma secondo me è di gran lunga la soluzione migliore :) Proveniente da Java, odio l'ereditarietà multipla ...
Edouard Berthe

4
@Edouardb unittest esegue solo classi a livello di modulo che ereditano da TestCase. Ma BaseTest non è a livello di modulo.
JoshB,

Come alternativa molto simile, potresti definire l'ABC all'interno di una funzione no-args che restituisce l'ABC quando viene chiamato
Anakhand

34

Puoi risolvere questo problema con un singolo comando:

del(BaseTest)

Quindi il codice sarebbe simile al seguente:

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 è un membro del modulo mentre viene definito, quindi è disponibile per l'uso come classe base dei SubTest. Appena prima che la definizione sia completa, del () la rimuove come membro, quindi il framework unittest non la troverà quando cerca sottoclassi TestCase nel modulo.
mhsmith,

3
questa è una risposta fantastica! Mi piace più di @MatthewMarshall perché nella sua soluzione, otterrai errori di sintassi da pylint, perché i self.assert*metodi non esistono in un oggetto standard.
SimplyKnownAsG

1
Non funziona se a BaseTest viene fatto riferimento in qualsiasi altro punto della classe base o delle sue sottoclassi, ad esempio quando si chiama super () nel metodo si sostituisce: super( BaseTest, cls ).setUpClass( )
Hannes,

1
@Hannes Almeno in Python 3, si BaseTestpuò fare riferimento attraverso super(self.__class__, self)o solo super()nelle sottoclassi, anche se apparentemente non se si dovessero ereditare i costruttori . Forse esiste anche un'alternativa "anonima" quando la classe base deve fare riferimento a se stessa (non che io abbia idea di quando una classe deve fare riferimento a se stessa).
Stein,

29

La risposta di Matthew Marshall è ottima, ma richiede che tu erediti da due classi in ciascuno dei tuoi casi di test, che è soggetto a errori. Invece, lo uso (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
È pulito. C'è un modo per aggirare dover usare un salto? Per me, i salti non sono desiderabili e vengono utilizzati per indicare un problema nel piano di test corrente (con il codice o il test)?
Zach Young,

@ZacharyYoung Non lo so, forse altre risposte possono aiutare.
Dennis Golomazov,

@ZacharyYoung Ho provato a risolvere questo problema, vedi la mia risposta.
simonzack

non è immediatamente chiaro cosa sia intrinsecamente soggetto a errori
nell'ereditare

@jwg vedi i commenti alla risposta accettata :) Devi ereditare ciascuna delle tue classi di test dalle due classi di base; è necessario preservarne l'ordine corretto; se desideri aggiungere un'altra classe di test di base, dovresti ereditare anche da essa. Non c'è niente di sbagliato nei mixin, ma in questo caso possono essere sostituiti con un semplice salto.
Dennis Golomazov,

7

Cosa stai cercando di ottenere? Se disponi di un codice di test comune (asserzioni, test dei modelli, ecc.), Inseriscili in metodi che non hanno il prefisso, testquindi unittestnon li caricherai.

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
Secondo il tuo suggerimento, common_assertion () verrebbe comunque eseguito automaticamente durante il test delle sottoclassi?
Stewart,

@Stewart No, non lo sarebbe. L'impostazione predefinita prevede solo l'esecuzione di metodi che iniziano con "test".
CS

6

La risposta di Matthew è quella che dovevo usare dato che sono ancora in 2.5. Ma a partire da 2.7 puoi usare il decoratore @ unittest.skip () su tutti i metodi di test che vuoi saltare.

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

Dovrai implementare il tuo decoratore di salti per verificare il tipo di base. Non ho mai usato questa funzione prima, ma dalla parte superiore della mia testa potresti usare BaseTest come tipo di marker per condizionare il salto:

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

6

Un modo in cui ho pensato di risolverlo è nascondere i metodi di test se si utilizza la classe base. In questo modo i test non vengono saltati, quindi i risultati del test possono essere verdi anziché gialli in molti strumenti di reporting dei test.

Rispetto al metodo di mixin, ide come PyCharm non si lamenterà del fatto che i metodi unit test mancano nella classe base.

Se una classe base eredita da questa classe, dovrà sovrascrivere i metodi setUpClasse 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

È possibile aggiungere la __test_ = Falseclasse BaseTest, ma se lo si aggiunge, tenere presente che è necessario aggiungere le __test__ = Trueclassi derivate per poter eseguire i test.

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

Questa soluzione non funziona con il test runner / test runner di Unittest. (Credo che richieda l'uso di un runner di prova alternativo, come il naso.)
medmunds

4

Un'altra opzione è di non eseguire

unittest.main()

Invece di quello che puoi usare

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

Quindi esegui solo i test nella classe TestClass


Questa è la soluzione meno confusa. Invece di modificare ciò che unittest.main()raccoglie nella suite predefinita, si forma una suite esplicita ed si eseguono i test.
zgoda,

1

Ho fatto lo stesso di @Vladim P. ( https://stackoverflow.com/a/25695512/2451329 ) ma leggermente modificato:

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

e eccoci.


1

A partire da Python 3.2, è possibile aggiungere una funzione test_loader a un modulo per controllare quali test (se presenti) vengono rilevati dal meccanismo di rilevamento test.

Ad esempio, quanto segue caricherà solo i poster SubTest1e i SubTest2casi di test originali, ignorando Base:

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

Dovrebbe essere possibile iterare standard_tests(una TestSuitecontenente le prove loader predefinito pensa) e copiare quasi Basea suiteposto, ma la natura nidificata di TestSuite.__iter__marchi che molto più complicata.


0

Rinomina il metodo testCommon in qualcos'altro. Unittest (di solito) salta tutto ciò che non ha "test" al suo interno.

Veloce e semplice

  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
Ciò avrebbe come risultato la mancata esecuzione del test methodCommon in nessuno dei sottotest.
Pepe Lebeck-Jobe,

0

Quindi questa è una specie di vecchio thread, ma oggi ho riscontrato questo problema e ho pensato al mio hack per questo. Usa un decoratore che rende i valori delle funzioni Nessuno quando acceduto attraverso la classe base. Non devi preoccuparti di setup e setupclass perché se la baseclass non ha test non funzioneranno.

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

0

Ecco una soluzione che utilizza solo funzionalità unittest documentate e che evita di avere uno stato "salta" nei risultati del test:

class BaseTest(unittest.TestCase):

    def __init__(self, methodName='runTest'):
        if self.__class__ is BaseTest:
            # don't run these tests in the abstract base implementation
            methodName = 'runNoTestsInBaseClass'
        super().__init__(methodName)

    def runNoTestsInBaseClass(self):
        pass

    def testCommon(self):
        # everything else as in the original question

Come funziona: secondo la unittest.TestCasedocumentazione , "Ogni istanza di TestCase eseguirà un singolo metodo di base: il metodo denominato methodName." Il "runTest" predefinito esegue tutti i metodi test * sulla classe, ecco come funzionano normalmente le istanze TestCase. Ma quando si esegue nella stessa classe base astratta, è possibile semplicemente ignorare quel comportamento con un metodo che non fa nulla.

Un effetto collaterale è che il conteggio dei test aumenta di uno: il "test" runNoTestsInBaseClass viene conteggiato come test riuscito quando viene eseguito su BaseClass.

(Funziona anche in Python 2.7, se ci sei ancora. Basta passare super()a super(BaseTest, self).)


-2

Modifica il nome del metodo BaseTest 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)

Produzione:

Ho eseguito 2 test in 0.000 secondi

Chiamata BaseTest: testCommon Chiamata
SubTest1: testSub1 Chiamata
BaseTest: testCommon Chiamata
SubTest2: testSub2

Dalla documentazione :

TestCase.setUp ()
Metodo chiamato per preparare l'apparecchiatura di prova. Questo viene chiamato immediatamente prima di chiamare il metodo di test; qualsiasi eccezione sollevata da questo metodo sarà considerata un errore piuttosto che un fallimento del test. L'implementazione predefinita non fa nulla.


Funzionerebbe, e se non avessi n testCommon, dovrei metterli tutti sotto setUp?
Thierry Lam,

1
Sì, dovresti inserire tutto il codice che non è un vero caso di test in setUp.
Brian R. Bondy,

Ma se una sottoclasse ha più di un test...metodo, setUpviene eseguita più e più volte, una volta per tale metodo; quindi NON è una buona idea fare dei test lì!
Alex Martelli,

Non sono sicuro di cosa OP volesse in termini di quando eseguito in uno scenario più complesso.
Brian R. Bondy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.