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).
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
También hay una Hipótesis que agrega pruebas basadas en fuzz o en propiedades: https://pypi.python.org/pypi/hypothesis
Este es un método de prueba muy poderoso.
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)