2008-12-07 5 views
14

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.¿Cómo puedo implementar de forma concisa múltiples pruebas de unidad similares en el marco de prueba de Python?

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 (sobre todo porque se pueden agregar más funciones 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() 

me ocurrió la idea para esto desde 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.

Miré 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 a la caja de prueba.

También miré dinámicamente adjuntando 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 por lo que sigue siendo trivial ampliar la lista de pruebas, al tiempo que se garantiza que se ejecuten todas las pruebas?

+0

pregunta relacionada: http://stackoverflow.com/questions/32899/how-to-generate-dynamic-unit-tests-in-python – jfs

Respuesta

4

Puede usar una metaclase para insertar dinámicamente las pruebas. Esto funciona bien para mí:

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

Gracias, esto funciona. Solo una leve rareza, la nariz no puede ver las pruebas añadidas por la metaclase. ¿Alguna sugerencia? – saffsd

+0

No estoy familiarizado con la nariz. Esto agrega los métodos a la clase, así que no estoy seguro de qué podría hacer la nariz para echarlos de menos. Sin embargo, sería interesante descubrir cuál es su magia. – Dustin

+0

cortarlo bien con el uso de 'f' en el __new__ de allí, es un poco oscuro –

1

Metaclasses es una opción. Otra opción es utilizar un 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 han probado. Pero debería funcionar bien.

+1

unittest.main() no recoge esto automáticamente, y tampoco lo hace la nariz. Además, FunctionTestCase llama a setUp sin argumentos, y las funciones_a_prueba deben estar envueltas en algo que confirme la prueba. – saffsd

11

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) 

añadir 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 funciones únicas se manejan de forma obvia. Nuevos métodos se agregan a la subclase.

es completamente compatible con unittest.main()

+0

Me gusta esta solución orientada a objetos. "Explicit es mejor que implícito" – muhuk

+0

No me gusta esto porque introduce un montón de código duplicado. Dado que cada una de las funciones está destinada a observar el mismo invariante que se está probando, me gustaría una forma de expresar exactamente esto sin tener que agruparlas en un solo caso de prueba. Tal vez debería? Gracias por la sugerencia, sin embargo. – saffsd

+0

Refactor código común en una superclase. Para eso son las superclases. Su "prueba común" es precisamente por qué tenemos superclases y subclases. –

-1

El problema con este enfoque es que si cualquier elemento de la lista no pasa la prueba , los elementos posteriores no reciben prueba.

Si lo considera desde el punto de vista de que, si una prueba falla, eso es crítico y su paquete completo no es válido, entonces no importa que otros elementos no se prueben, porque ' hey, tienes un error para arreglar '.

Una vez que pasa la prueba, se ejecutarán las otras pruebas.

Es cierto que hay información que se obtiene 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 de prueba es una falla de la aplicación.

+4

Lo reconozco, pero otro contraargumento es que si está ejecutando pruebas en un lote, por ejemplo, de la noche a la mañana, quiere saber dónde están todas las fallas, no solo la primera. – saffsd

5

Si ya está utilizando la nariz (y algunos de sus comentarios sugieren que lo son), ¿por qué no sólo tiene que utilizar Test Generators, que son la forma más fácil de aplicar 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 
1

El código metaclase anterior tiene problemas con la nariz porque wantMethod de la nariz en su selector.py es lo oking en un método de prueba dado __name__, no la tecla dict 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 
5

No tiene que usar Meta Classes aquí. Un simple lazo encaja perfectamente. Echar un vistazo al siguiente ejemplo:

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 para el TestCase1 tiene 3 métodos de ensayo que son apoyados por la nariz y la unittest.

+0

sí, encuentro que las metaclases para fines de "instrumentación" de las clases de un solo uso nunca vuelan bien, esto es un enfoque mucho mejor. –

0

He leído el ejemplo metaclase arriba, y me gustó, pero le faltaban dos cosas:

  1. cómo conducir 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, basado en datos, y en el que 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() 

Cuando se ejecuta el ejemplo anterior, si hay un error en el código (o datos de prueba equivocado), el mensaje de error contendrá nombre de la función, lo que debería ayudar en la depuración.

.....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) 
Cuestiones relacionadas