unittest unitarias test pruebas python unit-testing

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.



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:

  1. ¿Cómo conducirlo con una estructura de datos?
  2. ¿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)