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
ensetUp
?)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?
Réponses
Trop de publicités?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
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
.
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
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
Certains des outils disponibles pour faire des tests paramétrés en Python sont:
- Nez test générateurs (uniquement pour les tests de fonction, pas de cas de test classes)
- nez-paramétrées par David Wolever (également pour les cas de test classes)
- Unittest modèle par Boris Feld
- Paramétrées tests py.test
- paramétrées-cas de test par Austin Bingham
Voir également la question 1676269 pour plus de réponses à cette question.