Test unitaire Python avec base et sous-classe


149

J'ai actuellement quelques tests unitaires qui partagent un ensemble commun de tests. Voici un exemple:

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

Le résultat de ce qui précède est:

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

OK

Existe-t-il un moyen de réécrire ce qui précède afin que le tout premier testCommonne soit pas appelé?

EDIT: au lieu d'exécuter 5 tests ci-dessus, je veux qu'il n'exécute que 4 tests, 2 du SubTest1 et 2 autres de SubTest2. Il semble que Python unittest exécute seul le BaseTest d'origine et j'ai besoin d'un mécanisme pour empêcher que cela ne se produise.


Je vois que personne ne l'a mentionné mais avez-vous la possibilité de changer la partie principale et d'exécuter une suite de tests contenant toutes les sous-classes de BaseTest?
kon psych

Réponses:


154

Utilisez l'héritage multiple, de sorte que votre classe avec des tests communs n'hérite pas elle-même de 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
C'est la solution la plus élégante à ce jour.
Thierry Lam

27
Cette méthode ne fonctionne que pour les méthodes setUp et tearDown si vous inversez l'ordre des classes de base. Étant donné que les méthodes sont définies dans unittest.TestCase et qu'elles n'appellent pas super (), toutes les méthodes setUp et tearDown de CommonTests doivent être les premières dans le MRO, sinon elles ne seront pas appelées du tout.
Ian Clelland

32
Juste pour clarifier la remarque de Ian Clelland afin que ce soit plus clair pour des gens comme moi: si vous ajoutez des méthodes setUpet tearDownà la CommonTestsclasse, et que vous voulez qu'elles soient appelées pour chaque test dans les classes dérivées, vous devez inverser l'ordre des classes de base, de sorte que ce sera: class SubTest1(CommonTests, unittest.TestCase).
Dennis Golomazov

6
Je ne suis pas vraiment fan de cette approche. Cela établit un contrat dans le code dont les classes doivent hériter à la fois unittest.TestCase et CommonTests . Je pense que la setUpClassméthode ci-dessous est la meilleure et qu'elle est moins sujette à l'erreur humaine. Soit cela, soit envelopper la classe BaseTest dans une classe conteneur qui est un peu plus piratée mais évite le message de saut dans l'impression du test.
David Sanders

10
Le problème avec celui-ci est que pylint a un ajustement car il CommonTestsappelle des méthodes qui n'existent pas dans cette classe.
MadScientist

146

N'utilisez pas d'héritage multiple, cela vous mordra plus tard .

Au lieu de cela, vous pouvez simplement déplacer votre classe de base dans le module séparé ou l'envelopper avec la classe vide:

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

Le résultat:

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

OK

6
C'est mon préféré. C'est le moyen le moins piraté et n'interfère pas avec les méthodes de remplacement, ne modifie pas le MRO et me permet de définir setUp, setUpClass etc. dans la classe de base.
Hannes

6
Je ne comprends vraiment pas (d'où vient la magie?), Mais c'est de loin la meilleure solution selon moi :) Venant de Java, je déteste l'héritage multiple ...
Edouard Berthe

4
@Edouardb unittest exécute uniquement les classes de niveau module qui héritent de TestCase. Mais BaseTest n'est pas au niveau du module.
JoshB

Comme alternative très similaire, vous pouvez définir l'ABC dans une fonction sans argument qui renvoie l'ABC lorsqu'elle est appelée
Anakhand

34

Vous pouvez résoudre ce problème avec une seule commande:

del(BaseTest)

Donc, le code ressemblerait à ceci:

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 est membre du module pendant sa définition, il est donc disponible pour être utilisé comme classe de base des SubTests. Juste avant que la définition ne soit complète, del () le supprime en tant que membre, de sorte que le framework unittest ne le trouvera pas lorsqu'il recherche les sous-classes TestCase dans le module.
mhsmith

3
c'est une réponse géniale! Je l'aime plus que celle de @MatthewMarshall car dans sa solution, vous obtiendrez des erreurs de syntaxe de pylint, car les self.assert*méthodes n'existent pas dans un objet standard.
SimplyKnownAsG

1
Ne fonctionne pas si BaseTest est référencé ailleurs dans la classe de base ou ses sous-classes, par exemple lors de l'appel de super () dans les substitutions de méthode: super( BaseTest, cls ).setUpClass( )
Hannes

1
@Hannes Au moins en python 3, BaseTestpeut être référencé via super(self.__class__, self)ou simplement super()dans les sous-classes, mais apparemment pas si vous héritiez de constructeurs . Peut-être existe-t-il aussi une alternative «anonyme» lorsque la classe de base a besoin de se référencer elle-même (pas que je sache quand une classe doit se référencer elle-même).
Stein

29

La réponse de Matthew Marshall est excellente, mais elle nécessite que vous héritiez de deux classes dans chacun de vos cas de test, ce qui est sujet aux erreurs. Au lieu de cela, j'utilise ceci (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
C'est chouette. Existe-t-il un moyen de contourner l'utilisation d'une benne? Pour moi, les sauts ne sont pas souhaitables et sont utilisés pour indiquer un problème dans le plan de test actuel (soit avec le code, soit avec le test)?
Zach Young

@ZacharyYoung Je ne sais pas, peut-être que d'autres réponses peuvent aider.
Dennis Golomazov

@ZacharyYoung J'ai essayé de résoudre ce problème, voyez ma réponse.
simonzack

on ne sait pas immédiatement ce qui est intrinsèquement sujet aux erreurs dans l'héritage de deux classes
jwg

@jwg voir les commentaires sur la réponse acceptée :) Vous devez hériter chacune de vos classes de test des deux classes de base; vous devez en conserver le bon ordre; si vous souhaitez ajouter une autre classe de test de base, vous devez également en hériter. Il n'y a rien de mal avec les mixins, mais dans ce cas, ils peuvent être remplacés par un simple saut.
Dennis Golomazov

7

Qu'essayez-vous d'accomplir? Si vous avez du code de test commun (assertions, tests de modèle, etc.), placez-les dans des méthodes qui ne sont pas préfixées par testconséquent unittestne les chargeront pas.

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
Selon votre suggestion, est-ce que common_assertion () serait toujours exécuté automatiquement lors du test des sous-classes?
Stewart

@Stewart Non, ce ne serait pas le cas. Le paramètre par défaut consiste à exécuter uniquement les méthodes commençant par "test".
CS

6

La réponse de Matthew est celle que j'avais besoin d'utiliser puisque je suis toujours sur 2.5. Mais à partir de 2.7, vous pouvez utiliser le décorateur @ unittest.skip () sur toutes les méthodes de test que vous voulez ignorer.

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

Vous devrez implémenter votre propre décorateur de saut pour vérifier le type de base. Je n'ai jamais utilisé cette fonctionnalité auparavant, mais du haut de ma tête, vous pouvez utiliser BaseTest comme type de marqueur pour conditionner le saut:

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

6

J'ai pensé à résoudre ce problème en masquant les méthodes de test si la classe de base est utilisée. De cette façon, les tests ne sont pas ignorés, de sorte que les résultats des tests peuvent être verts au lieu de jaunes dans de nombreux outils de rapport de test.

Par rapport à la méthode mixin, les ide comme PyCharm ne se plaindront pas que les méthodes de test unitaire sont absentes de la classe de base.

Si une classe de base hérite de cette classe, elle devra remplacer les méthodes setUpClasset 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

Vous pouvez ajouter __test_ = Falseune classe BaseTest, mais si vous l'ajoutez, sachez que vous devez ajouter des __test__ = Trueclasses dérivées pour pouvoir exécuter des tests.

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

Cette solution ne fonctionne pas avec le propre test Discovery / Test Runner d'unittest. (Je pense que cela nécessite l'utilisation d'un autre
lanceur de

4

Une autre option est de ne pas exécuter

unittest.main()

Au lieu de cela, vous pouvez utiliser

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

Donc, vous n'exécutez que les tests dans la classe TestClass


C'est la solution la moins piratée. Au lieu de modifier ce qui est unittest.main()collecté dans la suite par défaut, vous formez une suite explicite et exécutez ses tests.
zgoda

1

J'ai fait à peu près la même chose que @Vladim P. ( https://stackoverflow.com/a/25695512/2451329 ) mais légèrement modifié:

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

et nous y voilà.


1

Depuis Python 3.2, vous pouvez ajouter une fonction test_loader à un module pour contrôler quels tests (le cas échéant) sont trouvés par le mécanisme de découverte de test.

Par exemple, ce qui suit ne chargera que l'affiche SubTest1et SubTest2les cas de test d'origine, en ignorant Base:

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

Il devrait être possible d'itérer standard_tests( TestSuitecontenant les tests trouvés par le chargeur par défaut) et de copier tout sauf Basevers suite, mais la nature imbriquée de TestSuite.__iter__rend cela beaucoup plus compliqué.


0

Renommez simplement la méthode testCommon en quelque chose d'autre. Unittest saute (généralement) tout ce qui ne contient pas de «test».

Rapide et simple

  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
Cela aurait pour résultat de ne pas exécuter le test methodCommon dans l'un des sous-tests.
Pepper Lebeck-Jobe

0

C'est donc une sorte de vieux fil mais je suis tombé sur ce problème aujourd'hui et j'ai pensé à mon propre hack pour cela. Il utilise un décorateur qui rend les valeurs des fonctions None lorsqu'il est accédé via la classe de base. Ne vous inquiétez pas de la configuration et de la classe de configuration car si la classe de base n'a pas de tests, elle ne s'exécutera pas.

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

Voici une solution qui n'utilise que des fonctionnalités unittest documentées et qui évite d'avoir un statut «skip» dans vos résultats de 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

Comment ça marche: selon la unittest.TestCasedocumentation , "Chaque instance de TestCase exécutera une seule méthode de base: la méthode nommée methodName." Les "runTests" par défaut exécutent toutes les méthodes de test * sur la classe - c'est ainsi que les instances de TestCase fonctionnent normalement. Mais lors de l'exécution dans la classe de base abstraite elle-même, vous pouvez simplement remplacer ce comportement par une méthode qui ne fait rien.

Un effet secondaire est que votre nombre de tests augmentera de un: le "test" runNoTestsInBaseClass est compté comme un test réussi lorsqu'il est exécuté sur BaseClass.

(Cela fonctionne également dans Python 2.7, si vous êtes toujours là-dessus. Il suffit de changer super()pour super(BaseTest, self).)


-2

Changez le nom de la méthode BaseTest en 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)

Production:

Ran 2 tests en 0,000s

Appel de BaseTest: testCommon Appel de
SubTest1: testSub1 Appel de
BaseTest: testCommon Appel de
SubTest2: testSub2

De la documentation :

TestCase.setUp ()
Méthode appelée pour préparer le montage de test. Ceci est appelé immédiatement avant d'appeler la méthode de test; toute exception déclenchée par cette méthode sera considérée comme une erreur plutôt que comme un échec de test. L'implémentation par défaut ne fait rien.


Cela fonctionnerait, et si j'avais n testCommon, devrais-je tous les placer sous setUp?
Thierry Lam

1
Oui, vous devez mettre tout le code qui n'est pas un cas de test réel sous setUp.
Brian R. Bondy

Mais si une sous-classe a plus d'une test...méthode, setUpest exécutée encore et encore et encore, une fois pour une telle méthode; ce n'est donc PAS une bonne idée d'y mettre des tests!
Alex Martelli

Je ne sais pas vraiment ce que OP voulait en termes d'exécution dans un scénario plus complexe.
Brian R. Bondy
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.