57 votes

Python unittest: générer plusieurs tests par programmation?

Double Possible:
Comment générer de la dynamique (paramétrées) des tests unitaires en python?

J'ai une fonction pour tester, under_test, et un ensemble de paires d'entrées/sorties:

[
(2, 332),
(234, 99213),
(9, 3),
# ...
]

Je voudrais que chacun de ces paires d'entrées/sorties pour être testé dans sa propre test_* méthode. Est-ce possible?

C'est en quelque sorte ce que je veux, mais le fait de forcer chaque paire d'entrée/sortie dans un seul test:

class TestPreReqs(unittest.TestCase):

    def setUp(self):
        self.expected_pairs = [(23, 55), (4, 32)]

    def test_expected(self):
        for exp in self.expected_pairs:
            self.assertEqual(under_test(exp[0]), exp[1])

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

(Aussi, dois-je vraiment envie de mettre la définition de la self.expected_pairs en setUp?)

Mise à JOUR: Essayer doublep's des conseils:

class TestPreReqs(unittest.TestCase):

    def setUp(self):
        expected_pairs = [
                          (2, 3),
                          (42, 11),
                          (3, None),
                          (31, 99),
                         ]

        for k, pair in expected_pairs:
            setattr(TestPreReqs, 'test_expected_%d' % k, create_test(pair))

    def create_test (pair):
        def do_test_expected(self):
            self.assertEqual(get_pre_reqs(pair[0]), pair[1])
        return do_test_expected


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

Cela ne fonctionne pas. 0 les tests sont exécutés. Ai-je adapter l'exemple de façon incorrecte?

57voto

Rory Points 8667

J'ai dû faire quelque chose de similaire. J'ai créé simple TestCase sous-classes qui ont pris une valeur dans leur __init__, comme ceci:

class KnownGood(unittest.TestCase):
    def __init__(self, input, output):
        super(KnownGood, self).__init__()
        self.input = input
        self.output = output
    def runTest(self):
        self.assertEqual(function_to_test(self.input), self.output)

J'ai ensuite fait un test de suite avec ces valeurs:

def suite():
    suite = unittest.TestSuite()
    suite.addTests(KnownGood(input, output) for input, output in known_values)
    return suite

Vous pouvez ensuite exécuter à partir de votre méthode principale:

if __name__ == '__main__':
    unittest.TextTestRunner().run(suite())

Les avantages sont:

  • Lorsque vous ajoutez plus de valeurs, le nombre de tests augmente, ce qui vous fait vous sentir comme vous le faites plus.
  • Chaque cas de test peut échouer individuellement
  • C'est conceptuellement simple, puisque chaque entrée/sortie de la valeur est convertie en un cas de test

41voto

doublep Points 9701

Pas testé:

 class TestPreReqs(unittest.TestCase):
    ...

def create_test (pair):
    def do_test_expected(self):
        self.assertEqual(under_test(pair[0]), pair[1])
    return do_test_expected

for k, pair in enumerate ([(23, 55), (4, 32)]):
    test_method = create_test (pair)
    test_method.__name__ = 'test_expected_%d' % k
    setattr (TestPreReqs, test_method.__name__, test_method)
 

Si vous l'utilisez souvent, vous pouvez améliorer cela en utilisant des fonctions utilitaires et / ou des décorateurs, je suppose. Notez que les paires ne sont pas un attribut de TestPreReqs objet dans cet exemple (et donc setUp disparu). Ils sont plutôt "câblés" dans un sens à la classe TestPreReqs .

27voto

Xavier Decoret Points 321

Comme souvent avec Python, il est compliqué pour fournir une solution simple.

Dans ce cas, on peut utiliser la métaprogrammation, décorateurs, et divers chouette Python astuces pour réaliser un bon résultat. Voici ce que le test final ressemblera à:

import unittest

# some magic code will be added here later

class DummyTest(unittest.TestCase):
  @for_examples(1, 2)
  @for_examples(3, 4)
  def test_is_smaller_than_four(self, value):
    self.assertTrue(value < 4)

  @for_examples((1,2),(2,4),(3,7))
  def test_double_of_X_is_Y(self, x, y):
    self.assertEqual(2 * x, y)

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

Lors de l'exécution de ce script, le résultat est:

..F...F
======================================================================
FAIL: test_double_of_X_is_Y(3,7)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/xdecoret/Documents/foo.py", line 22, in method_for_example
    method(self, *example)
  File "/Users/xdecoret/Documents/foo.py", line 41, in test_double_of_X_is_Y
    self.assertEqual(2 * x, y)
AssertionError: 6 != 7

======================================================================
FAIL: test_is_smaller_than_four(4)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/xdecoret/Documents/foo.py", line 22, in method_for_example
    method(self, *example)
  File "/Users/xdecoret/Documents/foo.py", line 37, in test_is_smaller_than_four
    self.assertTrue(value < 4)
AssertionError

----------------------------------------------------------------------
Ran 7 tests in 0.001s

FAILED (failures=2)

qui atteint notre objectif:

  • c'est normal: nous tirons de cas de test comme d'habitude
  • nous écrire des tests paramétrés qu'une seule fois
  • chaque exemple de valeur est considérée comme un test individuel
  • le décorateur peut être empilés, de sorte qu'il est facile d'utiliser des ensembles d'exemples (par exemple, à l'aide d'une fonction pour générer la liste de valeurs à partir de l'exemple de fichiers ou de répertoires)
  • cerise sur le gâteau, il travaille pour arbitraire arité de la signature

Comment cela fonctionne. Fondamentalement, le décorateur stocke les exemples dans un attribut de la fonction. Nous utilisons métaclasse à remplacer tous les décorés de la fonction avec une liste de fonctions. Et nous remplaçons le unittest.Cas de test avec notre nouveau Le code magique (à être collé dans la "magie" commentaire ci-dessus) est:

__examples__ = "__examples__"

def for_examples(*examples):
    def decorator(f, examples=examples):
      setattr(f, __examples__, getattr(f, __examples__,()) + examples)
      return f
    return decorator

class TestCaseWithExamplesMetaclass(type):
  def __new__(meta, name, bases, dict):
    def tuplify(x):
      if not isinstance(x, tuple):
        return (x,)
      return x
    for methodname, method in dict.items():
      if hasattr(method, __examples__):
        dict.pop(methodname)
        examples = getattr(method, __examples__)
        delattr(method, __examples__)
        for example in (tuplify(x) for x in examples):
          def method_for_example(self, method = method, example = example):
            method(self, *example)
          methodname_for_example = methodname + "(" + ", ".join(str(v) for v in example) + ")"
          dict[methodname_for_example] = method_for_example
    return type.__new__(meta, name, bases, dict)

class TestCaseWithExamples(unittest.TestCase):
  __metaclass__ = TestCaseWithExamplesMetaclass
  pass

unittest.TestCase = TestCaseWithExamples

Si quelqu'un veut le paquet de ce bien, ou de proposer un patch pour unittest, n'hésitez pas! Une citation de mon nom sera appréciée.

-- Edit --------

Le code peut être faite beaucoup plus simple et entièrement encapsulés dans le décorateur, si vous êtes prêt à utiliser un cadre d'introspection (importer le module sys)

def for_examples(*parameters):

  def tuplify(x):
    if not isinstance(x, tuple):
      return (x,)
    return x

  def decorator(method, parameters=parameters):
    for parameter in (tuplify(x) for x in parameters):

      def method_for_parameter(self, method=method, parameter=parameter):
        method(self, *parameter)
      args_for_parameter = ",".join(repr(v) for v in parameter)
      name_for_parameter = method.__name__ + "(" + args_for_parameter + ")"
      frame = sys._getframe(1)  # pylint: disable-msg=W0212
      frame.f_locals[name_for_parameter] = method_for_parameter
    return None
  return decorator

13voto

J.F. Sebastian Points 102961

nez (suggéré par @Paul Hankin)

#!/usr/bin/env python
# file: test_pairs_nose.py
from nose.tools import eq_ as eq

from mymodule import f

def test_pairs(): 
    for input, output in [ (2, 332), (234, 99213), (9, 3), ]:
        yield _test_f, input, output

def _test_f(input, output):
    try:
        eq(f(input), output)
    except AssertionError:
        if input == 9: # expected failure
            from nose.exc import SkipTest
            raise SkipTest("expected failure")
        else:
            raise

if __name__=="__main__":
   import nose; nose.main()

Exemple:

$ nosetests test_pairs_nose -v
test_pairs_nose.test_pairs(2, 332) ... ok
test_pairs_nose.test_pairs(234, 99213) ... ok
test_pairs_nose.test_pairs(9, 3) ... SKIP: expected failure

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK (SKIP=1)

unittest (approche similaire à @doublep un)

#!/usr/bin/env python
import unittest2 as unittest
from mymodule import f

def add_tests(generator):
    def class_decorator(cls):
        """Add tests to `cls` generated by `generator()`."""
        for f, input, output in generator():
            test = lambda self, i=input, o=output, f=f: f(self, i, o)
            test.__name__ = "test_%s(%r, %r)" % (f.__name__, input, output)
            setattr(cls, test.__name__, test)
        return cls
    return class_decorator

def _test_pairs():
    def t(self, input, output):
        self.assertEqual(f(input), output)

    for input, output in [ (2, 332), (234, 99213), (9, 3), ]:
        tt = t if input != 9 else unittest.expectedFailure(t)
        yield tt, input, output

class TestCase(unittest.TestCase):
    pass
TestCase = add_tests(_test_pairs)(TestCase)

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

Exemple:

$ python test_pairs_unit2.py -v
test_t(2, 332) (__main__.TestCase) ... ok
test_t(234, 99213) (__main__.TestCase) ... ok
test_t(9, 3) (__main__.TestCase) ... expected failure

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK (expected failures=1)

Si vous ne souhaitez pas installer unittest2 puis ajouter:

try:    
    import unittest2 as unittest
except ImportError:
    import unittest
    if not hasattr(unittest, 'expectedFailure'):
       import functools
       def _expectedFailure(func):
           @functools.wraps(func)
           def wrapper(*args, **kwargs):
               try:
                   func(*args, **kwargs)
               except AssertionError:
                   pass
               else:
                   raise AssertionError("UnexpectedSuccess")
           return wrapper
       unittest.expectedFailure = _expectedFailure    

7voto

akaihola Points 10007

Certains des outils disponibles pour faire des tests paramétrés en Python sont:

Voir également la question 1676269 pour plus de réponses à cette question.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X