python - tests - Escribir un método unittest.TestCase reutilizable(parametrizado)
python unittest skipif (6)
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 una manera 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)
- nose-parametrized por la nose-parametrized por David Wolever (también para las clases de TestCase)
- Plantilla más suave de Boris Feld
- Pruebas parametrizadas en py.test
- parametrized-testcase de parametrized-testcase por Austin Bingham
- DDT (Pruebas dirigidas por datos) por Carles Barrobés, para pruebas de unidad
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
.
Quizás algo como:
def test_many(self):
for n in range(0,1000):
self.assertEqual(self.somevalue, n)
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 .py
.F..FF....
======================================================================
FAIL: test_2 (__main__.ExampleTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File ".py", line 34, in test_function
function(self, value)
File ".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 ".py", line 34, in test_function
function(self, value)
File ".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 ".py", line 34, in test_function
function(self, value)
File ".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 más largo "test_multitest_gt_1" pero entonces, ¿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 la razón 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 answer :
"""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
<BLANKLINE>
======================================================================
FAIL: Test if self.somevalue equals 4
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: 2 != 4
<BLANKLINE>
======================================================================
FAIL: Test if self.somevalue equals 1
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: 2 != 1
<BLANKLINE>
======================================================================
FAIL: Test if self.somevalue equals 3
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: 2 != 3
<BLANKLINE>
----------------------------------------------------------------------
Ran 4 tests in ...s
<BLANKLINE>
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.