Python unittest: ¿Generar múltiples pruebas programáticamente?

Posible duplicado:
¿Cómo generar pruebas de unidad dinámicas (parametrizadas) en python?

Tengo una función para probar, under_test y un conjunto de pares de entrada / salida esperados:

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

Me gustaría que cada uno de estos pares de entrada / salida se probara en su propio método test_* . ¿Es eso posible?

Esto es algo de lo que quiero, pero forzando cada par de entrada / salida en una sola prueba:

 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() 

(Además, ¿realmente quiero poner esa definición de self.expected_pairs en la setUp ?)

ACTUALIZACIÓN: Intentando el consejo 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() 

Esto no funciona. Se ejecutan 0 pruebas. ¿He adaptado el ejemplo incorrectamente?

No probado:

 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 usas esto a menudo, podrías pretenderlo usando funciones de utilidad y / o decoradores, supongo. Tenga en cuenta que los pares no son un atributo del objeto TestPreReqs en este ejemplo (y, por lo setUp desaparece la setUp ). Más bien, están “cableados” en cierto sentido para la clase TestPreReqs .

Tuve que hacer algo similar. TestCase subclases simples de TestCase que tomaron un valor en su __init__ , como esto:

 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) 

Luego hice un conjunto de pruebas con estos valores:

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

A continuación, puede ejecutarlo desde su método principal:

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

Las ventajas de esto son:

  • A medida que agrega más valores, aumenta el número de pruebas reportadas, lo que le hace sentir que está haciendo más.
  • Cada caso de prueba individual puede fallar individualmente
  • Es conceptualmente simple, ya que cada valor de entrada / salida se convierte en un TestCase

Como suele ocurrir con Python, existe una forma complicada de proporcionar una solución simple.

En ese caso, podemos usar metaprogtwigción, decoradores y varios ingeniosos trucos de Python para lograr un buen resultado. Aquí es cómo se verá la prueba 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 __name__ == "__main__": unittest.main() 

Al ejecutar este script, el resultado es:

 ..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) 

Lo que logra nuestro objective:

  • es discreto: derivamos de TestCase como siempre
  • Escribimos pruebas parametrizadas solo una vez.
  • Cada valor de ejemplo se considera una prueba individual.
  • el decorador se puede astackr, por lo que es fácil usar conjuntos de ejemplos (por ejemplo, usar una función para construir la lista de valores a partir de archivos de ejemplo o directorios)
  • la guinda del pastel, funciona por arbitrariedad de la firma

Entonces, cómo funciona. Básicamente, el decorador almacena los ejemplos en un atributo de la función. Usamos metaclase para reemplazar cada función decorada con una lista de funciones. Y reemplazamos el testtest.TestCase con nuestro nuevo código mágico (que se pegará en el comentario "mágico" anterior) es:

 __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 alguien quiere empaquetar esto bien, o proponer un parche para unittest, ¡siéntase libre! Una cita de mi nombre será apreciada.

- Editar --------

El código puede ser mucho más simple y completamente encapsulado en el decorador si está listo para usar la introspección de marcos (importar el módulo 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 

nariz (sugerido por @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() 

Ejemplo:

 $ 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) 

test unitario (enfoque similar al 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.__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() 

Ejemplo:

 $ 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 no desea instalar unittest2 , agregue:

 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 

Algunas de las herramientas disponibles para realizar pruebas parametrizadas en Python son:

  • Generadores de prueba de nariz (solo para pruebas de función, no clases de TestCase)
  • parametrizado por la nariz por David Wolever (también para las clases de TestCase)
  • Plantilla más suave de Boris Feld
  • Pruebas parametrizadas en py.test
  • caja de prueba parametrizada por Austin Bingham

Vea también la pregunta 1676269 para obtener más respuestas a esta pregunta.

Creo que la solución de Rory es la más limpia y la más corta. Sin embargo, esta variación de “crear funciones sintéticas en un TestCase” de doublep también funciona:

 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'])