unitarias - ¿Cómo puedo implementar de forma concisa múltiples pruebas de unidad similares en el marco de prueba de Python?
test en python (9)
Estoy implementando pruebas unitarias para una familia de funciones que comparten varias invariantes. Por ejemplo, llamar a la función con dos matrices produce una matriz de forma conocida.
Me gustaría escribir pruebas unitarias para probar toda la familia de funciones para esta propiedad, sin tener que escribir un caso de prueba individual para cada función (particularmente dado que más funciones podrían agregarse más adelante).
Una forma de hacer esto sería iterar sobre una lista de estas funciones:
import unittest
import numpy
from somewhere import the_functions
from somewhere.else import TheClass
class Test_the_functions(unittest.TestCase):
def setUp(self):
self.matrix1 = numpy.ones((5,10))
self.matrix2 = numpy.identity(5)
def testOutputShape(unittest.TestCase):
"""Output of functions be of a certain shape"""
for function in all_functions:
output = function(self.matrix1, self.matrix2)
fail_message = "%s produces output of the wrong shape" % str(function)
self.assertEqual(self.matrix1.shape, output.shape, fail_message)
if __name__ == "__main__":
unittest.main()
Tengo la idea para esto de Dive Into Python . Allí, no es una lista de funciones que se están probando, sino una lista de pares de entrada y salida conocidos. El problema con este enfoque es que si algún elemento de la lista falla la prueba, los elementos posteriores no se prueban.
Analicé subclassing unittest.TestCase y, de alguna manera, proporcioné la función específica para probar como argumento, pero hasta donde sé, eso nos impide usar unittest.main () porque no habría forma de pasar el argumento al caso de prueba.
También miré dinámicamente unir funciones "testSomething" a la caja de prueba, usando setattr con lamdba, pero el caso de prueba no las reconoció.
¿Cómo puedo volver a escribir esto para que siga siendo trivial ampliar la lista de pruebas, mientras se asegura que se ejecuten todas las pruebas?
Metaclasses es una opción. Otra opción es usar TestSuite
:
import unittest
import numpy
import funcs
# get references to functions
# only the functions and if their names start with "matrixOp"
functions_to_test = [v for k,v in funcs.__dict__ if v.func_name.startswith(''matrixOp'')]
# suplly an optional setup function
def setUp(self):
self.matrix1 = numpy.ones((5,10))
self.matrix2 = numpy.identity(5)
# create tests from functions directly and store those TestCases in a TestSuite
test_suite = unittest.TestSuite([unittest.FunctionTestCase(f, setUp=setUp) for f in functions_to_test])
if __name__ == "__main__":
unittest.main()
No lo he probado. Pero debería funcionar bien.
Veo que esta pregunta es vieja. No estoy seguro de eso en aquel entonces, pero hoy tal vez podrías usar algunos paquetes de "pruebas basadas en datos":
Podría usar una metaclase para insertar dinámicamente las pruebas. Esto funciona bien para mi:
import unittest
class UnderTest(object):
def f1(self, i):
return i + 1
def f2(self, i):
return i + 2
class TestMeta(type):
def __new__(cls, name, bases, attrs):
funcs = [t for t in dir(UnderTest) if t[0] == ''f'']
def doTest(t):
def f(slf):
ut=UnderTest()
getattr(ut, t)(3)
return f
for f in funcs:
attrs[''test_gen_'' + f] = doTest(f)
return type.__new__(cls, name, bases, attrs)
class T(unittest.TestCase):
__metaclass__ = TestMeta
def testOne(self):
self.assertTrue(True)
if __name__ == ''__main__'':
unittest.main()
Este es mi enfoque favorito para la "familia de pruebas relacionadas". Me gustan las subclases explícitas de un TestCase que expresa las características comunes.
class MyTestF1( unittest.TestCase ):
theFunction= staticmethod( f1 )
def setUp(self):
self.matrix1 = numpy.ones((5,10))
self.matrix2 = numpy.identity(5)
def testOutputShape( self ):
"""Output of functions be of a certain shape"""
output = self.theFunction(self.matrix1, self.matrix2)
fail_message = "%s produces output of the wrong shape" % (self.theFunction.__name__,)
self.assertEqual(self.matrix1.shape, output.shape, fail_message)
class TestF2( MyTestF1 ):
"""Includes ALL of TestF1 tests, plus a new test."""
theFunction= staticmethod( f2 )
def testUniqueFeature( self ):
# blah blah blah
pass
class TestF3( MyTestF1 ):
"""Includes ALL of TestF1 tests with no additional code."""
theFunction= staticmethod( f3 )
Agregue una función, agregue una subclase de MyTestF1
. Cada subclase de MyTestF1 incluye todas las pruebas en MyTestF1 sin código duplicado de ningún tipo.
Las características únicas se manejan de una manera obvia. Nuevos métodos se agregan a la subclase.
Es completamente compatible con unittest.main()
El problema con este enfoque es que si algún elemento de la lista falla la prueba, los elementos posteriores no se prueban.
Si lo miras desde el punto de vista de que, si una prueba falla, eso es crítico y tu paquete completo no es válido, entonces no importa que otros elementos no se prueben, porque ''hey, tienes un error arreglar''.
Una vez que la prueba pase, las otras pruebas se ejecutarán.
Es cierto que hay información que puede obtenerse del conocimiento de que otras pruebas están fallando, y eso puede ayudar con la depuración, pero aparte de eso, supongamos que cualquier falla en la prueba es una falla completa de la aplicación.
Si ya está usando nose (y algunos de sus comentarios lo sugieren), ¿por qué no usa Test Generators , que es la forma más directa de implementar pruebas paramétricas que he encontrado?
Por ejemplo:
from binary_search import search1 as search
def test_binary_search():
data = (
(-1, 3, []),
(-1, 3, [1]),
(0, 1, [1]),
(0, 1, [1, 3, 5]),
(1, 3, [1, 3, 5]),
(2, 5, [1, 3, 5]),
(-1, 0, [1, 3, 5]),
(-1, 2, [1, 3, 5]),
(-1, 4, [1, 3, 5]),
(-1, 6, [1, 3, 5]),
(0, 1, [1, 3, 5, 7]),
(1, 3, [1, 3, 5, 7]),
(2, 5, [1, 3, 5, 7]),
(3, 7, [1, 3, 5, 7]),
(-1, 0, [1, 3, 5, 7]),
(-1, 2, [1, 3, 5, 7]),
(-1, 4, [1, 3, 5, 7]),
(-1, 6, [1, 3, 5, 7]),
(-1, 8, [1, 3, 5, 7]),
)
for result, n, ns in data:
yield check_binary_search, result, n, ns
def check_binary_search(expected, n, ns):
actual = search(n, ns)
assert expected == actual
Produce:
$ nosetests -d
...................
----------------------------------------------------------------------
Ran 19 tests in 0.009s
OK
El código de la metaclase anterior tiene problemas con nose porque nose wantMethod en su selector.py está mirando el __name__
de un método de prueba determinado, no la clave dict del atributo.
Para usar un método de prueba definido por metaclass con nose, el nombre del método y la clave del diccionario deben ser iguales, y el prefijo debe ser detectado por la nariz (es decir, con ''test_'').
# test class that uses a metaclass
class TCType(type):
def __new__(cls, name, bases, dct):
def generate_test_method():
def test_method(self):
pass
return test_method
dct[''test_method''] = generate_test_method()
return type.__new__(cls, name, bases, dct)
class TestMetaclassed(object):
__metaclass__ = TCType
def test_one(self):
pass
def test_two(self):
pass
No tiene que usar Meta Clases aquí. Un simple lazo encaja perfectamente. Eche un vistazo al ejemplo a continuación:
import unittest
class TestCase1(unittest.TestCase):
def check_something(self, param1):
self.assertTrue(param1)
def _add_test(name, param1):
def test_method(self):
self.check_something(param1)
setattr(TestCase1, ''test_''+name, test_method)
test_method.__name__ = ''test_''+name
for i in range(0, 3):
_add_test(str(i), False)
Una vez que se ejecuta el for, TestCase1 tiene 3 métodos de prueba que son compatibles con nosetest y unittest.
He leído el ejemplo de metaclass anterior, y me gustó, pero me faltaban dos cosas:
- ¿Cómo conducirlo con una estructura de datos?
- ¿Cómo asegurarse de que la función de prueba está escrita correctamente?
Escribí este ejemplo más completo, que está basado en datos, y en el cual la función de prueba está probada por sí misma.
import unittest
TEST_DATA = (
(0, 1),
(1, 2),
(2, 3),
(3, 5), # This intentionally written to fail
)
class Foo(object):
def f(self, n):
return n + 1
class FooTestBase(object):
"""Base class, defines a function which performs assertions.
It defines a value-driven check, which is written as a typical function, and
can be tested.
"""
def setUp(self):
self.obj = Foo()
def value_driven_test(self, number, expected):
self.assertEquals(expected, self.obj.f(number))
class FooTestBaseTest(unittest.TestCase):
"""FooTestBase has a potentially complicated, data-driven function.
It needs to be tested.
"""
class FooTestExample(FooTestBase, unittest.TestCase):
def runTest(self):
return self.value_driven_test
def test_value_driven_test_pass(self):
test_base = self.FooTestExample()
test_base.setUp()
test_base.value_driven_test(1, 2)
def test_value_driven_test_fail(self):
test_base = self.FooTestExample()
test_base.setUp()
self.assertRaises(
AssertionError,
test_base.value_driven_test, 1, 3)
class DynamicTestMethodGenerator(type):
"""Class responsible for generating dynamic test functions.
It only wraps parameters for specific calls of value_driven_test. It could
be called a form of currying.
"""
def __new__(cls, name, bases, dct):
def generate_test_method(number, expected):
def test_method(self):
self.value_driven_test(number, expected)
return test_method
for number, expected in TEST_DATA:
method_name = "testNumbers_%s_and_%s" % (number, expected)
dct[method_name] = generate_test_method(number, expected)
return type.__new__(cls, name, bases, dct)
class FooUnitTest(FooTestBase, unittest.TestCase):
"""Combines generated and hand-written functions."""
__metaclass__ = DynamicTestMethodGenerator
if __name__ == ''__main__'':
unittest.main()
Al ejecutar el ejemplo anterior, si hay un error en el código (o datos de prueba incorrectos), el mensaje de error contendrá el nombre de la función, lo que debería ayudar a la eliminación de fallas.
.....F
======================================================================
FAIL: testNumbers_3_and_5 (__main__.FooUnitTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "dyn_unittest.py", line 65, in test_method
self.value_driven_test(number, expected)
File "dyn_unittest.py", line 30, in value_driven_test
self.assertEquals(expected, self.obj.f(number))
AssertionError: 5 != 4
----------------------------------------------------------------------
Ran 6 tests in 0.002s
FAILED (failures=1)