web-dev-qa-db-fra.com

cas de test abstrait utilisant python unittest

Est-il possible de créer un résumé TestCase, qui comportera des méthodes test_ *, mais cette TestCase ne sera pas appelée et ces méthodes ne seront utilisées que dans des sous-classes? Je pense que je vais avoir un résumé TestCase dans ma suite de tests et il sera sous-classé pour quelques implémentations différentes d’une même interface. C’est la raison pour laquelle toutes les méthodes de test ne sont qu’un, un seul, changement de méthode interne. Comment puis-je le faire de manière élégante?

41
gruszczy

Je ne comprenais pas bien ce que vous aviez prévu de faire - La règle à suivre est "ne pas être intelligent avec les tests" - Les avoir juste là, écrits simplement.

Mais pour obtenir ce que vous voulez, si vous héritez de unittest.TestCase, chaque fois que vous appelez unittest.main (), votre classe "abstraite" sera exécutée. Je pense que c'est la situation que vous souhaitez éviter.

Faites juste ceci: Créez votre classe "abstraite" héritant de "objet", pas de TestCase. Et pour les implémentations «concrètes», utilisez simplement un héritage multiple: Hériter de unittest.TestCase et de votre classe abstraite.

import unittest

class Abstract(object):
    def test_a(self):
        print "Running for class", self.__class__

class Test(Abstract, unittest.TestCase):
    pass

unittest.main()

update : a inversé l'ordre d'héritage - Abstract en premier afin que ses définitions ne soient pas remplacées par les TestCase valeurs par défaut, comme indiqué dans les commentaires ci-dessous.

57
jsbueno

L'héritage multiple n'est pas une excellente option ici, principalement pour les deux raisons suivantes:

  1. Aucune des méthodes de TestCase n'utilise super(). Vous devez donc d'abord répertorier votre classe pour que des méthodes telles que setUp() et tearDown() fonctionnent.
  2. pylint avertira que la classe de base utilise self.assertEquals() etc. qui ne sont pas définis sur self à ce stade.

Voici le problème que j'ai trouvé: transformez run() en un no-op pour la classe de base uniquement.

class TestBase( unittest.TestCase ):

  def __init__( self, *args, **kwargs ):
    super( TestBase, self ).__init__( *args, **kwargs )
    self.helper = None
    # Kludge alert: We want this class to carry test cases without being run
    # by the unit test framework, so the `run' method is overridden to do
    # nothing.  But in order for sub-classes to be able to do something when
    # run is invoked, the constructor will rebind `run' from TestCase.
    if self.__class__ != TestBase:
      # Rebind `run' from the parent class.
      self.run = unittest.TestCase.run.__get__( self, self.__class__ )                          
    else:
      self.run = lambda self, *args, **kwargs: None

  def newHelper( self ):
    raise NotImplementedError()

  def setUp( self ):
    print "shared for all subclasses"
    self.helper = self.newHelper()

  def testFoo( self ):
    print "shared for all subclasses"
    # test something with self.helper

class Test1( TestBase ):
  def newHelper( self ):
    return HelperObject1()

class Test2( TestBase ):
  def newHelper( self ):
    return HelperObject2()
10
tsuna

Juste pour mettre mes deux sous, bien que cela va probablement à l’encontre de certaines conventions, vous pouvez définir votre test élémentaire abstrait comme un membre protégé pour empêcher son exécution. J'ai implémenté les éléments suivants dans Django et fonctionne comme requis. Voir exemple ci-dessous.

from Django.test import TestCase


class _AbstractTestCase(TestCase):

    """
    Abstract test case - should not be instantiated by the test runner.
    """

    def test_1(self):
        raise NotImplementedError()

    def test_2(self):
        raise NotImplementedError()


class TestCase1(_AbstractTestCase):

    """
    This test case will pass and fail.
    """

    def test_1(self):
        self.assertEqual(1 + 1, 2)


class TestCase2(_AbstractTestCase):

    """
    This test case will pass successfully.
    """

    def test_1(self):
        self.assertEqual(2 + 2, 4)

    def test_2(self):
        self.assertEqual(12 * 12, 144)
6
Dan Ward

Il y a un moyen très simple que tout le monde a raté jusqu'à présent. Et contrairement à plusieurs réponses, cela fonctionne avec les pilotes all test, plutôt que d’échouer à la minute où vous basculez entre eux.

Utilisez simplement l'héritage comme d'habitude, puis ajoutez:

del AbstractTestCase

à la fin du module.

5
o11c

Si vous suivez la convention consistant à répertorier explicitement toutes les classes de test dans run_unittest (voir, par exemple, la suite de tests Python pour de nombreuses utilisations de cette convention), il sera alors direct de not listant une classe spécifique.

Si vous souhaitez continuer à utiliser unittest.main et si vous pouvez autoriser l’utilisation de unittest2 (par exemple, à partir de Python 2.7), vous pouvez utiliser son protocole load_tests pour spécifier les classes contenant des scénarios de test. Dans les versions antérieures, vous devrez sous-classer TestLoader et substituer loadTestsFromModule .

4
Martin v. Löwis

La bibliothèque Python unittest possède le protocole load_tests , qui peut être utilisé pour réaliser exactement ce que vous voulez:

# Add this function to module with AbstractTestCase class
def load_tests(loader, tests, _):
    result = []
    for test_case in tests:
        if type(test_case._tests[0]) is AbstractTestCase:
            continue
        result.append(test_case)
    return loader.suiteClass(result)
2
uglide

Si vous voulez vraiment utiliser l'héritage au lieu de mixins, une solution simple consiste à imbriquer le test abstrait dans une autre classe.

Cela évite les problèmes liés à la découverte de test runner et vous pouvez toujours importer le test abstrait depuis un autre module.

import unittest

class AbstractTests(object):
    class AbstractTest(unittest.TestCase)
        def test_a(self):
            print "Running for class", self.__class__

class Test(AbstractTests.AbstractTest):
    pass
1
jrobichaud

Une autre raison de vouloir faire ce que le PO fait est de créer une classe de base hautement paramétrée qui implémente une grande partie d'un ensemble de tests de base qui doivent être reproduits dans plusieurs environnements/scénarios. Ce que je décris consiste essentiellement à créer un appareil paramétré, à la pytest, en utilisant unittest.

En supposant que vous (comme moi) décidiez de vous échapper aussi vite que possible des solutions basées sur plusieurs héritages, vous pourriez avoir le problème suivant en utilisant load_tests () pour filtrer votre classe de base de la suite chargée:

Dans TestLoader standard, load_tests est appelé after le chargement automatique à partir de la classe est effectué. Parce que: * Ce chargement automatique à partir de la classe tentera de construire des instances à partir de votre classe de base avec la signature standard init (self, name), et * Vous peut vouloir que cette classe de base ait une signature de ctor très différente, ou * vous voudrez peut-être ignorer construction-puis-suppression de vos instances de classe de base pour une autre raison

.. vous voudrez peut-être empêcher complètement ce chargement automatique d'instances de test à partir de classes de base. 

EDIT: La solution de Vadim dans cet autre fil est une manière plus élégante, concise et indépendante de le faire. J'ai implémenté "l'astuce de classe imbriquée" et confirmé que cela fonctionne parfaitement dans le but d'empêcher TestLoader de "trouver" vos bases TestCase.

Je l'avais initialement fait en modifiant TestLoader.loadTestsFromModule pour ignorer simplement les classes TestCase qui servent de classes de base pour toutes les autres classes TestCase du module:

for name in dir(module):
    obj = getattr(module, name)
    # skip TestCase classes:
    # 1. without any test methods defined
    # 2. that are base classes
    #    (we don't allow instantiating TestCase base classes, which allows test designers
    #     to implement actual test methods in highly-parametrized base classes.)
    if isinstance(obj, type) and issubclass(obj, unittest.TestCase) and \
            self.getTestCaseNames(obj) and not isbase(obj, module):
        loaded_suite = self.loadTestsFromTestCase(obj)
        # ignore empty suites
        if loaded_suite.countTestCases():
            tests.append(loaded_suite)

où:

def isbase(cls, module):
    '''Returns True if cls is base class to any classes in module, else False.'''
    for name in dir(module):
        obj = getattr(module, name)
        if obj is not cls and isinstance(obj, type) and issubclass(obj, cls):
            return True
    return False

La paramétrisation dont j'ai parlé ci-dessus est implémentée en faisant en sorte que chaque classe enfant définisse ses détails de fixture (les paramètres) et les passe à la classe de base TestCase ctor afin que toutes ses méthodes impl communes (les "fixture") setUp */tearDown */cleanup * _ {et les méthodes de test elles-mêmes) ont toutes les informations qui définissent le projecteur désormais très spécifique sur lequel cette classe TestCase enfant doit fonctionner.

Pour moi, il s’agissait d’une solution temporaire pour la mise en place rapide de certains appareils paramétrés en unittest, car j’ai l’intention de déplacer les tests de mon équipe afin de les tester au plus vite.

0
timblaktu