web-dev-qa-db-fra.com

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

Duplicata possible:
Comment générer des tests unitaires dynamiques (paramétrés) en python?

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

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

Je voudrais que chacune de ces paires d'entrée/sortie soit testée dans son propre test_* méthode. Est-ce possible?

C'est en quelque sorte ce que je veux, mais en forçant 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()

(De plus, je veux vraiment mettre cette définition de self.expected_pairs dans setUp?)

MISE À JOUR: Essayer les conseils de doublep :

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 test est exécuté. Ai-je mal adapté l'exemple?

47
Nick Heiner

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.__= '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 l'objet TestPreReqs dans cet exemple (et donc setUp a disparu). Ils sont plutôt "câblés" dans un sens à la classe TestPreReqs.

33
doublep

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

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 une suite de tests avec ces valeurs:

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

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

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

Les avantages de ceci sont:

  • Au fur et à mesure que vous ajoutez des valeurs, le nombre de tests signalés augmente, ce qui vous donne l'impression d'en faire plus.
  • Chaque cas de test individuel peut échouer individuellement
  • C'est conceptuellement simple, car chaque valeur d'entrée/sortie est convertie en un TestCase
49
Rory

Comme souvent avec Python, il existe un moyen compliqué de fournir une solution simple.

Dans ce cas, nous pouvons utiliser la métaprogrammation, les décorateurs et diverses astuces astucieuses Python pour obtenir un bon résultat. Voici à quoi ressemblera le test final:

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 __== "__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:

  • il est discret: nous dérivons de TestCase comme d'habitude
  • nous écrivons les tests paramétrés une seule fois
  • chaque exemple de valeur est considéré comme un test individuel
  • le décorateur peut être empilé, il est donc facile d'utiliser des ensembles d'exemples (par exemple, en utilisant une fonction pour construire la liste de valeurs à partir d'exemples de fichiers ou de répertoires)
  • cerise sur le gâteau, ça marche pour une arité arbitraire de la signature

Alors, comment ça marche. Fondamentalement, le décorateur stocke les exemples dans un attribut de la fonction. Nous utilisons la métaclasse pour remplacer chaque fonction décorée par une liste de fonctions. Et nous remplaçons le unittest.TestCase par notre nouveau Le code magique (à coller dans le commentaire "magic" 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 bien emballer cela, ou proposer un patch pour unittest, n'hésitez pas! Une citation de mon nom sera appréciée.

-- Éditer --------

Le code peut être rendu beaucoup plus simple et entièrement encapsulé dans le décorateur si vous êtes prêt à utiliser l'introspection d'images (importez 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.__+ "(" + args_for_parameter + ")"
      frame = sys._getframe(1)  # pylint: disable-msg=W0212
      frame.f_locals[name_for_parameter] = method_for_parameter
    return None
  return decorator
25
Xavier Decoret

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 à celle de @ doublep )

#!/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.__= "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    
11
jfs

Certains des outils disponibles pour effectuer des tests paramétrés dans Python sont:

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

6
akaihola
2
user97370

Je pense que la solution de Rory est la plus propre et la plus courte. Cependant, cette variation de doublep "créer des fonctions synthétiques dans un TestCase" fonctionne également:

from functools import partial
class TestAllReports(unittest.TestCase):
    pass

def test_spamreport(name):
    assert classify(getSample(name))=='spamreport', name

for rep in REPORTS:
    testname = 'test_'+rep
    testfunc = partial(test_spamreport, rep)
    testfunc.__doc__ = testname
    setattr( TestAllReports, testname, testfunc )

if __name__=='__main__':
    unittest.main(argv=sys.argv + ['--verbose'])
1
johntellsall