Escribiendo un método de prueba de unidad reutilizable (parametrizado). TestCase

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

Estoy escribiendo pruebas utilizando el paquete unittest, y quiero evitar el código repetido. Voy a realizar una serie de pruebas que requieren un método muy similar, pero con un solo valor diferente cada vez. Un ejemplo simplista e inútil sería:

class ExampleTestCase(unittest.TestCase): def test_1(self): self.assertEqual(self.somevalue, 1) def test_2(self): self.assertEqual(self.somevalue, 2) def test_3(self): self.assertEqual(self.somevalue, 3) def test_4(self): self.assertEqual(self.somevalue, 4) 

¿Hay alguna forma de escribir el ejemplo anterior sin repetir todo el código cada vez, sino escribir un método genérico, por ejemplo?

  def test_n(self, n): self.assertEqual(self.somevalue, n) 

y diciendo a unittest que pruebe esta prueba con diferentes entradas?

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
  • DDT (Pruebas dirigidas por datos) por Carles Barrobés, para pruebas de unidad

Si realmente desea tener múltiples pruebas de unidad, entonces necesita múltiples métodos. La única forma de conseguirlo es a través de algún tipo de generación de código. Puede hacerlo a través de metaclases o ajustando la clase después de la definición, incluyendo (si está utilizando Python 2.6) a través de un decorador de clase.

Aquí hay una solución que busca los miembros especiales ‘multitest’ y ‘multitest_values’ y los usa para construir los métodos de prueba sobre la marcha. No elegante, pero hace más o menos lo que quieres:

 import unittest import inspect class SomeValue(object): def __eq__(self, other): return other in [1, 3, 4] class ExampleTestCase(unittest.TestCase): somevalue = SomeValue() multitest_values = [1, 2, 3, 4] def multitest(self, n): self.assertEqual(self.somevalue, n) multitest_gt_values = "ABCDEF" def multitest_gt(self, c): self.assertTrue(c > "B", c) def add_test_cases(cls): values = {} functions = {} # Find all the 'multitest*' functions and # matching list of test values. for key, value in inspect.getmembers(cls): if key.startswith("multitest"): if key.endswith("_values"): values[key[:-7]] = value else: functions[key] = value # Put them together to make a list of new test functions. # One test function for each value for key in functions: if key in values: function = functions[key] for i, value in enumerate(values[key]): def test_function(self, function=function, value=value): function(self, value) name ="test%s_%d" % (key[9:], i+1) test_function.__name__ = name setattr(cls, name, test_function) add_test_cases(ExampleTestCase) if __name__ == "__main__": unittest.main() 

Esta es la salida de cuando la ejecuto.

 % python stackoverflow.py .F..FF.... ====================================================================== FAIL: test_2 (__main__.ExampleTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "stackoverflow.py", line 34, in test_function function(self, value) File "stackoverflow.py", line 13, in multitest self.assertEqual(self.somevalue, n) AssertionError: <__main__.SomeValue object at 0xd9870> != 2 ====================================================================== FAIL: test_gt_1 (__main__.ExampleTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "stackoverflow.py", line 34, in test_function function(self, value) File "stackoverflow.py", line 17, in multitest_gt self.assertTrue(c > "B", c) AssertionError: A ====================================================================== FAIL: test_gt_2 (__main__.ExampleTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "stackoverflow.py", line 34, in test_function function(self, value) File "stackoverflow.py", line 17, in multitest_gt self.assertTrue(c > "B", c) AssertionError: B ---------------------------------------------------------------------- Ran 10 tests in 0.001s FAILED (failures=3) 

Inmediatamente puede ver algunos de los problemas que ocurren con la generación de código. ¿De dónde viene “test_gt_1”? Podría cambiar el nombre al “test_multitest_gt_1” más largo, pero ¿qué prueba es 1? Mejor sería comenzar desde _0 en lugar de _1, y quizás en su caso sepa que los valores se pueden usar como un nombre de función de Python.

No me gusta este enfoque. He trabajado en bases de código que generaron métodos de prueba de forma automática (en un caso utilizando una metaclase) y descubrí que era mucho más difícil de entender de lo que era útil. Cuando una prueba falló, fue difícil averiguar la fuente del caso de falla, y fue difícil mantener el código de depuración para probar el motivo de la falla.

(Los errores de depuración en el ejemplo que escribí aquí no son tan difíciles como el enfoque de metaclase específico con el que tuve que trabajar).

Supongo que lo que quieres es “pruebas parametrizadas”.

No creo que el módulo unittest admita esto (desafortunadamente), pero si estuviera agregando esta característica, se vería algo como esto:

 # Will run the test for all combinations of parameters @RunTestWith(x=[0, 1, 2, 3], y=[-1, 0, 1]) def testMultiplication(self, x, y): self.assertEqual(multiplication.multiply(x, y), x*y) 

Con el módulo unittest existente, un decorador simple como este no podrá “replicar” la prueba varias veces, pero creo que esto es factible usando una combinación de un decorador y una metaclase (la metaclase debe observar todos los métodos de ‘prueba *’ y replicar (bajo diferentes nombres generados automáticamente) aquellos que tienen un decorador aplicado).

Un enfoque más orientado a los datos podría ser más claro que el utilizado en la respuesta de Andrew Dalke :

 """Parametrized unit test. Builds a single TestCase class which tests if its `somevalue` method is equal to the numbers 1 through 4. This is accomplished by creating a list (`cases`) of dictionaries which contain test specifications and then feeding the list to a function which creates a test case class. When run, the output shows that three of the four cases fail, as expected: >>> import sys >>> from unittest import TextTestRunner >>> run_tests(TextTestRunner(stream=sys.stdout, verbosity=9)) ... # doctest: +ELLIPSIS Test if self.somevalue equals 4 ... FAIL Test if self.somevalue equals 1 ... FAIL Test if self.somevalue equals 3 ... FAIL Test if self.somevalue equals 2 ... ok  ====================================================================== FAIL: Test if self.somevalue equals 4 ---------------------------------------------------------------------- Traceback (most recent call last): ... AssertionError: 2 != 4  ====================================================================== FAIL: Test if self.somevalue equals 1 ---------------------------------------------------------------------- Traceback (most recent call last): ... AssertionError: 2 != 1  ====================================================================== FAIL: Test if self.somevalue equals 3 ---------------------------------------------------------------------- Traceback (most recent call last): ... AssertionError: 2 != 3  ---------------------------------------------------------------------- Ran 4 tests in ...s  FAILED (failures=3) """ from unittest import TestCase, TestSuite, defaultTestLoader cases = [{'name': "somevalue_equals_one", 'doc': "Test if self.somevalue equals 1", 'value': 1}, {'name': "somevalue_equals_two", 'doc': "Test if self.somevalue equals 2", 'value': 2}, {'name': "somevalue_equals_three", 'doc': "Test if self.somevalue equals 3", 'value': 3}, {'name': "somevalue_equals_four", 'doc': "Test if self.somevalue equals 4", 'value': 4}] class BaseTestCase(TestCase): def setUp(self): self.somevalue = 2 def test_n(self, n): self.assertEqual(self.somevalue, n) def make_parametrized_testcase(class_name, base_classes, test_method, cases): def make_parametrized_test_method(name, value, doc=None): def method(self): return test_method(self, value) method.__name__ = "test_" + name method.__doc__ = doc return (method.__name__, method) test_methods = (make_parametrized_test_method(**case) for case in cases) class_dict = dict(test_methods) return type(class_name, base_classes, class_dict) TestCase = make_parametrized_testcase('TestOneThroughFour', (BaseTestCase,), test_n, cases) def make_test_suite(): load = defaultTestLoader.loadTestsFromTestCase return TestSuite(load(TestCase)) def run_tests(runner): runner.run(make_test_suite()) if __name__ == '__main__': from unittest import TextTestRunner run_tests(TextTestRunner(verbosity=9)) 

No estoy seguro de qué vudú está involucrado en la determinación del orden en que se ejecutan las pruebas, pero el doctest pasa consistentemente para mí, al menos.

Para situaciones más complejas, es posible reemplazar el elemento de values de los diccionarios de cases con una tupla que contiene una lista de argumentos y un dictado de argumentos de palabras clave. Aunque en ese punto básicamente estás codificando lisp en python.

Quizás algo como:

 def test_many(self): for n in range(0,1000): self.assertEqual(self.somevalue, n) 

Escriba un único método de prueba que realice todas sus pruebas y capture todos los resultados, escriba sus propios mensajes de diagnóstico en stderr y suspenda la prueba si falla alguna de sus subpruebas:

 def test_with_multiple_parameters(self): failed = False for k in sorted(self.test_parameters.keys()): if not self.my_test(self.test_parameters[k]): print >> sys.stderr, "Test {0} failed.".format(k) failed = True self.assertFalse(failed) 

Tenga en cuenta que, por supuesto, el nombre de my_test() no puede comenzar con la test .