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?
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
.
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:
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:
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
#!/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)
#!/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
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.
Avec des tests de nez, alors oui. Voir ceci: https://nose.readthedocs.org/en/latest/writing_tests.html#test-generators
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'])