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

Tengo algún tipo de datos de prueba y quiero crear una prueba de unidad para cada elemento. Mi primera idea fue hacerlo así:

import unittest l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]] class TestSequence(unittest.TestCase): def testsample(self): for name, a,b in l: print "test", name self.assertEqual(a,b) if __name__ == '__main__': unittest.main() 

La desventaja de esto es que maneja todos los datos en una prueba. Me gustaría generar una prueba para cada elemento sobre la marcha. ¿Alguna sugerencia?

Yo uso algo como esto:

 import unittest l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]] class TestSequense(unittest.TestCase): pass def test_generator(a, b): def test(self): self.assertEqual(a,b) return test if __name__ == '__main__': for t in l: test_name = 'test_%s' % t[0] test = test_generator(t[1], t[2]) setattr(TestSequense, test_name, test) unittest.main() 

El paquete parameterized se puede utilizar para automatizar este proceso:

 from parameterized import parameterized class TestSequence(unittest.TestCase): @parameterized.expand([ ["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"], ]) def test_sequence(self, name, a, b): self.assertEqual(a,b) 

Lo que generará las pruebas:

 test_sequence_0_foo (__main__.TestSequence) ... ok test_sequence_1_bar (__main__.TestSequence) ... FAIL test_sequence_2_lee (__main__.TestSequence) ... ok ====================================================================== FAIL: test_sequence_1_bar (__main__.TestSequence) ---------------------------------------------------------------------- Traceback (most recent call last): File "/usr/local/lib/python2.7/site-packages/parameterized/parameterized.py", line 233, in  standalone_func = lambda *a: func(*(a + p.args), **p.kwargs) File "x.py", line 12, in test_sequence self.assertEqual(a,b) AssertionError: 'a' != 'b' 

Usando unittest (desde 3.4)

Desde Python 3.4, el paquete de biblioteca unittest estándar tiene el subTest contexto subTest .

Vea la documentación:

  • 26.4.7. Distinguir iteraciones de prueba usando subpruebas
  • subprueba

Ejemplo:

 from unittest import TestCase param_list = [('a', 'a'), ('a', 'b'), ('b', 'b')] class TestDemonstrateSubtest(TestCase): def test_works_as_expected(self): for p1, p2 in param_list: with self.subTest(): self.assertEqual(p1, p2) 

También puede especificar un mensaje personalizado y valores de parámetros para subTest() :

 with self.subTest(msg="Checking if p1 equals p2", p1=p1, p2=p2): 

Usando la nariz

El marco de prueba de la nariz apoya esto .

Ejemplo (el código a continuación es el contenido completo del archivo que contiene la prueba):

 param_list = [('a', 'a'), ('a', 'b'), ('b', 'b')] def test_generator(): for params in param_list: yield check_em, params[0], params[1] def check_em(a, b): assert a == b 

La salida del comando nosetests:

 > nosetests -v testgen.test_generator('a', 'a') ... ok testgen.test_generator('a', 'b') ... FAIL testgen.test_generator('b', 'b') ... ok ====================================================================== FAIL: testgen.test_generator('a', 'b') ---------------------------------------------------------------------- Traceback (most recent call last): File "/usr/lib/python2.5/site-packages/nose-0.10.1-py2.5.egg/nose/case.py", line 203, in runTest self.test(*self.arg) File "testgen.py", line 7, in check_em assert a == b AssertionError ---------------------------------------------------------------------- Ran 3 tests in 0.006s FAILED (failures=1) 

Esto se puede resolver elegantemente usando Metaclasses:

 import unittest l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]] class TestSequenceMeta(type): def __new__(mcs, name, bases, dict): def gen_test(a, b): def test(self): self.assertEqual(a, b) return test for tname, a, b in l: test_name = "test_%s" % tname dict[test_name] = gen_test(a,b) return type.__new__(mcs, name, bases, dict) class TestSequence(unittest.TestCase): __metaclass__ = TestSequenceMeta if __name__ == '__main__': unittest.main() 

A partir de Python 3.4 se han introducido subpruebas a unittest para este propósito. Consulte la documentación para más detalles. TestCase.subTest es un administrador de contexto que le permite a uno aislar las afirmaciones en una prueba para que una falla se informe con información de parámetros pero no detenga la ejecución de la prueba. Aquí está el ejemplo de la documentación:

 class NumbersTest(unittest.TestCase): def test_even(self): """ Test that numbers between 0 and 5 are all even. """ for i in range(0, 6): with self.subTest(i=i): self.assertEqual(i % 2, 0) 

La salida de una ejecución de prueba sería:

 ====================================================================== FAIL: test_even (__main__.NumbersTest) (i=1) ---------------------------------------------------------------------- Traceback (most recent call last): File "subtests.py", line 32, in test_even self.assertEqual(i % 2, 0) AssertionError: 1 != 0 ====================================================================== FAIL: test_even (__main__.NumbersTest) (i=3) ---------------------------------------------------------------------- Traceback (most recent call last): File "subtests.py", line 32, in test_even self.assertEqual(i % 2, 0) AssertionError: 1 != 0 ====================================================================== FAIL: test_even (__main__.NumbersTest) (i=5) ---------------------------------------------------------------------- Traceback (most recent call last): File "subtests.py", line 32, in test_even self.assertEqual(i % 2, 0) AssertionError: 1 != 0 

Esto también es parte de unittest2 , por lo que está disponible para versiones anteriores de Python.

load_tests es un mecanismo poco conocido introducido en 2.7 para crear dinámicamente un TestSuite. Con él, puedes crear fácilmente pruebas parametrizadas.

Por ejemplo:

 import unittest class GeneralTestCase(unittest.TestCase): def __init__(self, methodName, param1=None, param2=None): super(GeneralTestCase, self).__init__(methodName) self.param1 = param1 self.param2 = param2 def runTest(self): pass # Test that depends on param 1 and 2. def load_tests(loader, tests, pattern): test_cases = unittest.TestSuite() for p1, p2 in [(1, 2), (3, 4)]: test_cases.addTest(GeneralTestCase('runTest', p1, p2)) return test_cases 

Ese código ejecutará todos los TestCases en TestSuite devueltos por load_tests. El mecanismo de descubrimiento no ejecuta automáticamente ninguna otra prueba.

Alternativamente, también puede usar la herencia como se muestra en este ticket: http://bugs.python.org/msg151444

Se puede hacer usando pytest . Solo escribe el archivo test_me.py con contenido:

 import pytest @pytest.mark.parametrize('name, left, right', [['foo', 'a', 'a'], ['bar', 'a', 'b'], ['baz', 'b', 'b']]) def test_me(name, left, right): assert left == right, name 

Y ejecute su prueba con el comando py.test --tb=short test_me.py . Entonces la salida se verá como:

 =========================== test session starts ============================ platform darwin -- Python 2.7.6 -- py-1.4.23 -- pytest-2.6.1 collected 3 items test_me.py .F. ================================= FAILURES ================================= _____________________________ test_me[bar-ab] _____________________________ test_me.py:8: in test_me assert left == right, name E AssertionError: bar ==================== 1 failed, 2 passed in 0.01 seconds ==================== 

Es simple !. También pytest tiene más características como fixtures , mark , assert , etc.

Usa la librería ddt . Añade decoradores simples para los métodos de prueba:

 import unittest from ddt import ddt, data from mycode import larger_than_two @ddt class FooTestCase(unittest.TestCase): @data(3, 4, 12, 23) def test_larger_than_two(self, value): self.assertTrue(larger_than_two(value)) @data(1, -3, 2, 0) def test_not_larger_than_two(self, value): self.assertFalse(larger_than_two(value)) 

Esta biblioteca se puede instalar con pip . No requiere nose , y funciona excelente con el módulo de unittest unidad de biblioteca estándar.

Usted se beneficiaría de probar la biblioteca de TestScenarios .

testscenarios proporciona una dependency injection limpia para las pruebas de estilo de prueba de unidad de Python. Esto se puede usar para probar la interfaz (probar muchas implementaciones a través de un solo conjunto de pruebas) o para la dependency injection clásica (proporcionar pruebas con dependencias externas al código de prueba en sí, lo que permite realizar pruebas sencillas en diferentes situaciones).

También hay hipótesis que agrega pruebas basadas en fuzz o propiedad: https://pypi.python.org/pypi/hypothesis

Este es un método de prueba muy poderoso.

Puede utilizar el complemento nose-ittr ( pip install nose-ittr ).

Es muy fácil de integrar con las pruebas existentes, se requieren cambios mínimos (si los hay). También es compatible con la nariz multiprocesamiento plugin.

No es que también pueda tener una función de setup personalizada por prueba.

 @ittr(number=[1, 2, 3, 4]) def test_even(self): assert_equal(self.number % 2, 0) 

También es posible pasar los parámetros más nosetest , como con sus nosetest complemento, de esta manera solo puede ejecutar una prueba específica con un parámetro específico:

 nosetest -a number=2 

Me encontré con ParamUnittest el otro día cuando miraba el código fuente del radón ( ejemplo de uso en el repository de github ). Debería funcionar con otros marcos que extiendan TestCase (como Nose).

Aquí hay un ejemplo:

 import unittest import paramunittest @paramunittest.parametrized( ('1', '2'), #(4, 3), <---- uncomment to have a failing test ('2', '3'), (('4', ), {'b': '5'}), ((), {'a': 5, 'b': 6}), {'a': 5, 'b': 6}, ) class TestBar(TestCase): def setParameters(self, a, b): self.a = a self.b = b def testLess(self): self.assertLess(self.a, self.b) 

Uso metaclases y decoradores para generar pruebas. Puedes comprobar mi implementación en python_wrap_cases . Esta biblioteca no requiere ningún marco de prueba.

Tu ejemplo

 import unittest from python_wrap_cases import wrap_case @wrap_case class TestSequence(unittest.TestCase): @wrap_case("foo", "a", "a") @wrap_case("bar", "a", "b") @wrap_case("lee", "b", "b") def testsample(self, name, a, b): print "test", name self.assertEqual(a, b) 

Salida de consola:

 testsample_u'bar'_u'a'_u'b' (tests.example.test_stackoverflow.TestSequence) ... test bar FAIL testsample_u'foo'_u'a'_u'a' (tests.example.test_stackoverflow.TestSequence) ... test foo ok testsample_u'lee'_u'b'_u'b' (tests.example.test_stackoverflow.TestSequence) ... test lee ok 

También puedes usar generadores . Por ejemplo, este código genera todas las combinaciones posibles de pruebas con los argumentos a__list y b__list

 import unittest from python_wrap_cases import wrap_case @wrap_case class TestSequence(unittest.TestCase): @wrap_case(a__list=["a", "b"], b__list=["a", "b"]) def testsample(self, a, b): self.assertEqual(a, b) 

Salida de consola:

 testsample_a(u'a')_b(u'a') (tests.example.test_stackoverflow.TestSequence) ... ok testsample_a(u'a')_b(u'b') (tests.example.test_stackoverflow.TestSequence) ... FAIL testsample_a(u'b')_b(u'a') (tests.example.test_stackoverflow.TestSequence) ... FAIL testsample_a(u'b')_b(u'b') (tests.example.test_stackoverflow.TestSequence) ... ok 

Solo usa metaclases, como se ve aquí;

 class DocTestMeta(type): """ Test functions are generated in metaclass due to the way some test loaders work. For example, setupClass() won't get called unless there are other existing test methods, and will also prevent unit test loader logic being called before the test methods have been defined. """ def __init__(self, name, bases, attrs): super(DocTestMeta, self).__init__(name, bases, attrs) def __new__(cls, name, bases, attrs): def func(self): """Inner test method goes here""" self.assertTrue(1) func.__name__ = 'test_sample' attrs[func.__name__] = func return super(DocTestMeta, cls).__new__(cls, name, bases, attrs) class ExampleTestCase(TestCase): """Our example test case, with no methods defined""" __metaclass__ = DocTestMeta 

Salida:

 test_sample (ExampleTestCase) ... OK 
 import unittest def generator(test_class, a, b): def test(self): self.assertEqual(a, b) return test def add_test_methods(test_class): #First element of list is variable "a", then variable "b", then name of test case that will be used as suffix. test_list = [[2,3, 'one'], [5,5, 'two'], [0,0, 'three']] for case in test_list: test = generator(test_class, case[0], case[1]) setattr(test_class, "test_%s" % case[2], test) class TestAuto(unittest.TestCase): def setUp(self): print 'Setup' pass def tearDown(self): print 'TearDown' pass _add_test_methods(TestAuto) # It's better to start with underscore so it is not detected as a test itself if __name__ == '__main__': unittest.main(verbosity=1) 

RESULTADO:

 >>> Setup FTearDown Setup TearDown .Setup TearDown . ====================================================================== FAIL: test_one (__main__.TestAuto) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:/inchowar/Desktop/PyTrash/test_auto_3.py", line 5, in test self.assertEqual(a, b) AssertionError: 2 != 3 ---------------------------------------------------------------------- Ran 3 tests in 0.019s FAILED (failures=1) 

Puedes usar TestSuite y clases personalizadas de TestCase .

 import unittest class CustomTest(unittest.TestCase): def __init__(self, name, a, b): super().__init__() self.name = name self.a = a self.b = b def runTest(self): print("test", self.name) self.assertEqual(self.a, self.b) if __name__ == '__main__': suite = unittest.TestSuite() suite.addTest(CustomTest("Foo", 1337, 1337)) suite.addTest(CustomTest("Bar", 0xDEAD, 0xC0DE)) unittest.TextTestRunner().run(suite) 

He estado teniendo problemas con un estilo muy particular de pruebas parametrizadas. Todas nuestras pruebas de Selenium pueden ejecutarse localmente, pero también deberían poder ejecutarse de forma remota en varias plataformas en SauceLabs. Básicamente, quería tomar una gran cantidad de casos de prueba ya escritos y parametrizarlos con el menor número posible de cambios en el código. Además, necesitaba poder pasar los parámetros al método de configuración, algo que no he visto ninguna solución para otro lado.

Esto es lo que he encontrado:

 import inspect import types test_platforms = [ {'browserName': "internet explorer", 'platform': "Windows 7", 'version': "10.0"}, {'browserName': "internet explorer", 'platform': "Windows 7", 'version': "11.0"}, {'browserName': "firefox", 'platform': "Linux", 'version': "43.0"}, ] def sauce_labs(): def wrapper(cls): return test_on_platforms(cls) return wrapper def test_on_platforms(base_class): for name, function in inspect.getmembers(base_class, inspect.isfunction): if name.startswith('test_'): for platform in test_platforms: new_name = '_'.join(list([name, ''.join(platform['browserName'].title().split()), platform['version']])) new_function = types.FunctionType(function.__code__, function.__globals__, new_name, function.__defaults__, function.__closure__) setattr(new_function, 'platform', platform) setattr(base_class, new_name, new_function) delattr(base_class, name) return base_class 

Con esto, todo lo que tenía que hacer era agregar un decorador simple @sauce_labs () a cada TestCase antiguo normal, y ahora, al ejecutarlos, se envuelven y se vuelven a escribir, de modo que todos los métodos de prueba se parametrizan y se les cambia el nombre. LoginTests.test_login (self) se ejecuta como LoginTests.test_login_internet_explorer_10.0 (self), LoginTests.test_login_internet_explorer_11.0 (self) y LoginTests.test_login_firefox_43.0 (self), y cada uno tiene el mismo valor en el que se encuentra. plataforma contra la que se debe ejecutar, incluso en LoginTests.setUp, que es crucial para mi tarea, ya que ahí es donde se inicializa la conexión a SauceLabs.

De todos modos, espero que esto pueda ser de ayuda para alguien que quiera realizar una parametrización “global” similar de sus pruebas.

Esta solución funciona con unittest y nose :

 #!/usr/bin/env python import unittest def make_function(description, a, b): def ghost(self): self.assertEqual(a, b, description) print description ghost.__name__ = 'test_{0}'.format(description) return ghost class TestsContainer(unittest.TestCase): pass testsmap = { 'foo': [1, 1], 'bar': [1, 2], 'baz': [5, 5]} def generator(): for name, params in testsmap.iteritems(): test_func = make_function(name, params[0], params[1]) setattr(TestsContainer, 'test_{0}'.format(name), test_func) generator() if __name__ == '__main__': unittest.main() 

Las respuestas basadas en metaclase todavía funcionan en Python3, pero en lugar del atributo __metaclass__ uno tiene que usar el parámetro metaclass , como en:

 class ExampleTestCase(TestCase,metaclass=DocTestMeta): pass 

La meta-progtwigción es divertida, pero puede ponerse en camino. La mayoría de las soluciones aquí hacen que sea difícil:

  • lanzar selectivamente una prueba
  • apuntar de nuevo al código dado el nombre de la prueba

Entonces, mi primera sugerencia es seguir la ruta simple / explícita (funciona con cualquier corredor de prueba):

 import unittest class TestSequence(unittest.TestCase): def _test_complex_property(self, a, b): self.assertEqual(a,b) def test_foo(self): self._test_complex_property("a", "a") def test_bar(self): self._test_complex_property("a", "b") def test_lee(self): self._test_complex_property("b", "b") if __name__ == '__main__': unittest.main() 

Ya que no debemos repetirnos, mi segunda sugerencia se basa en la respuesta de @Javier: adoptar pruebas basadas en propiedades. Biblioteca de hipótesis:

  • es “más despiadado de la generación de casos de prueba que nosotros, los simples humanos”
  • proporcionará ejemplos de conteo simples
  • funciona con cualquier corredor de prueba
  • Tiene muchas más características interesantes (estadísticas, resultados de prueba adicionales, …)

    clase TestSequence (unittest.TestCase):

     @given(st.text(), st.text()) def test_complex_property(self, a, b): self.assertEqual(a,b) 

Para probar sus ejemplos específicos, simplemente agregue:

  @example("a", "a") @example("a", "b") @example("b", "b") 

Para ejecutar solo un ejemplo particular, puede comentar los otros ejemplos (siempre que el ejemplo se ejecute primero). Es posible que desee utilizar @given(st.nothing()) . Otra opción es reemplazar todo el bloque por:

  @given(st.just("a"), st.just("b")) 

Ok, no tienes nombres de prueba distintos. Pero tal vez solo necesitas:

  • Un nombre descriptivo de la propiedad bajo prueba.
  • qué entrada conduce al fracaso (ejemplo de falsificación).

Ejemplo mas divertido

Muy tarde para la fiesta, pero tuve problemas para hacer que esto funcionara para setUpClass .

Aquí hay una versión de la respuesta de setUpClass que da acceso a setUpClass a atributos asignados dinámicamente.

 import unittest class GeneralTestCase(unittest.TestCase): @classmethod def setUpClass(cls): print '' print cls.p1 print cls.p2 def runTest1(self): self.assertTrue((self.p2 - self.p1) == 1) def runTest2(self): self.assertFalse((self.p2 - self.p1) == 2) def load_tests(loader, tests, pattern): test_cases = unittest.TestSuite() for p1, p2 in [(1, 2), (3, 4)]: clsname = 'TestCase_{}_{}'.format(p1, p2) dct = { 'p1': p1, 'p2': p2, } cls = type(clsname, (GeneralTestCase,), dct) test_cases.addTest(cls('runTest1')) test_cases.addTest(cls('runTest2')) return test_cases 

Salidas

 1 2 .. 3 4 .. ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK 

Además de usar setattr, podemos usar load_tests desde python 3.2. Consulte la publicación del blog blog.livreuro.com/en/coding/python/how-to-generate-discoverable-unit-tests-in-python-dynamically/

 class Test(unittest.TestCase): pass def _test(self, file_name): open(file_name, 'r') as f: self.assertEqual('test result',f.read()) def _generate_test(file_name): def test(self): _test(self, file_name) return test def _generate_tests(): for file in files: file_name = os.path.splitext(os.path.basename(file))[0] setattr(Test, 'test_%s' % file_name, _generate_test(file)) test_cases = (Test,) def load_tests(loader, tests, pattern): _generate_tests() suite = TestSuite() for test_class in test_cases: tests = loader.loadTestsFromTestCase(test_class) suite.addTests(tests) return suite if __name__ == '__main__': _generate_tests() unittest.main() 

La siguiente es mi solución. Encuentro esto útil cuando: 1. Debería funcionar para unittest.Testcase y unittest discover 2. Tenga un conjunto de pruebas que se ejecutarán para diferentes configuraciones de parámetros. 3. Muy simple, sin dependencia de otros paquetes importar unittest

  class BaseClass(unittest.TestCase): def setUp(self): self.param = 2 self.base = 2 def test_me(self): self.assertGreaterEqual(5, self.param+self.base) def test_me_too(self): self.assertLessEqual(3, self.param+self.base) class Child_One(BaseClass): def setUp(self): BaseClass.setUp(self) self.param = 4 class Child_Two(BaseClass): def setUp(self): BaseClass.setUp(self) self.param = 1