unittest unitarias pruebas automatizadas python unit-testing parameterized-unit-test

automatizadas - ¿Cómo se generan pruebas unitarias dinámicas(parametrizadas) en python?



pruebas unitarias en python (22)

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 artículo sobre la marcha. ¿Alguna sugerencia?


A partir de Python 3.4 subtests se han introducido a unittest para este fin. Ver la documentación para más detalles. TestCase.subTest es un gestor de contexto que permite aislar 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)

El resultado 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.


Además de usar setattr, podemos usar load_test 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()


El marco de prueba de nose compatible con esto .

Ejemplo (el siguiente código 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)

ACTUALIZACIÓN: soporte unittest desde 3.4:

Desde Python 3.4, el paquete de subTest unidad de biblioteca estándar tiene el subTest contexto subTest , lo que permite una funcionalidad similar con solo unittest .

Ver la documentación:

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


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


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


He estado teniendo problemas con un estilo muy particular de pruebas parametrizadas. Todas nuestras pruebas de Selenium pueden ejecutarse localmente, pero también deben poder ejecutarse de forma remota contra 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 setUp, algo que no he visto ninguna solución para otro lado.

Esto es lo que se me ocurrió:

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 tuve que hacer fue agregar un decorador simple @sauce_labs () a cada TestCase antiguo normal, y ahora cuando los ejecuto, se envuelven y se reescriben, de modo que todos los métodos de prueba se parametrizan y cambian de 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 parámetro self.platform para decidir qué navegador / plataforma contra la cual ejecutar, incluso en LoginTests.setUp, que es crucial para mi tarea, ya que ahí es donde se inicia la conexión a SauceLabs.

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


La metaprogramación es divertida, pero puede ponerse en camino. La mayoría de las soluciones aquí dificultan:

  • lanzar selectivamente una prueba
  • señalar de nuevo el código dado el nombre de la prueba

Entonces, mi primera sugerencia es seguir el camino simple / explícito (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()

Como no deberíamos repetirnos, mi segunda sugerencia se basa en la respuesta de @ Javier: abra las pruebas basadas en la propiedad. Biblioteca de hipótesis:

  • es "más implacablemente tortuoso en la generación de casos de prueba que nosotros, simples humanos"
  • proporcionará simples ejemplos de conteo
  • funciona con cualquier corredor de prueba
  • tiene muchas más características interesantes (estadísticas, resultados de pruebas adicionales, ...)

    clase TestSequence (unittest.TestCase):

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

Para probar tus ejemplos específicos, solo agrega:

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

Para ejecutar solo un ejemplo en particular, puede comentar los otros ejemplos (el ejemplo proporcionado se ejecutará 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 a la falla (ejemplo de falsificación).

Ejemplo de Funnier


La siguiente es mi solución. Esto me resulta útil cuando: 1. Debe funcionar para la prueba de unidad. Descubrimiento de prueba y prueba de unidad 2. Realice una serie de pruebas para diferentes configuraciones de parámetros. 3. Muy simple sin dependencia de otros paquetes import 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


Las respuestas basadas en metaclass 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


Me encontré con ParamUnittest el otro día cuando miraba el código fuente del radon ( ejemplo de uso en el repositorio github ). Debería funcionar con otros frameworks que extienden 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)


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


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

Es muy fácil de integrar con las pruebas existentes, se requieren cambios mínimos (si corresponde). También es compatible con el complemento de multiproceso nariz .

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 de nosetest como con su plugin de build-in attrib , de esta manera puede ejecutar solo una prueba específica con un parámetro específico:

nosetest -a number=2


Súper tarde para la fiesta, pero tuve problemas para hacer que esto funcione para setUpClass .

Aquí hay una versión de la respuesta de @ Javier que le da acceso a setUpClass a los 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


Se puede hacer mediante el uso de Pytest . Simplemente escriba el archivo test_me.py con el 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á así:

=========================== 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-a-b] _____________________________ test_me.py:8: in test_me assert left == right, name E AssertionError: bar ==================== 1 failed, 2 passed in 0.01 seconds ====================

¡Es simple !. Además, pytest tiene más características como fixtures , mark , assert , etc.


Solo use 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



Te beneficiarías al probar la biblioteca TestScenarios .

testscenarios proporciona inyección de dependencia limpia para las pruebas de estilo de prueba de python unittest. Esto se puede usar para pruebas de interfaz (probando muchas implementaciones a través de un conjunto de pruebas único) o para inyección de dependencia clásica (proporcionar pruebas con dependencias externas al código de prueba en sí, lo que permite realizar pruebas fácilmente en diferentes situaciones).


Use la biblioteca ddt . Agrega 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 de manera excelente con el módulo de unittest biblioteca estándar.


Utilizo metaclases y decoradores para generar pruebas. Puede verificar mi implementación 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 la consola:

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

También puedes usar generadores . Por ejemplo, este código genera todas las posibles combinaciones de pruebas con 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 la consola:

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


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 nose-parameterized se puede usar para automatizar este proceso:

from nose_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)

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/nose_parameterized/parameterized.py", line 233, in <lambda> 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''


load_tests es un mecanismo poco conocido introducido en 2.7 para crear dinámicamente un TestSuite. Con él, puede 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 otras pruebas.

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


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)