Prueba unitaria de Python con base y subclase


149

Actualmente tengo algunas pruebas unitarias que comparten un conjunto común de pruebas. Aquí hay un ejemplo:

import unittest

class BaseTest(unittest.TestCase):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

La salida de lo anterior es:

Calling BaseTest:testCommon
.Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

¿Hay alguna manera de reescribir lo anterior para que testCommonno se llame al primero ?

EDITAR: en lugar de ejecutar 5 pruebas anteriores, quiero que ejecute solo 4 pruebas, 2 de SubTest1 y otras 2 de SubTest2. Parece que Python unittest está ejecutando el BaseTest original por sí solo y necesito un mecanismo para evitar que eso suceda.


Veo que nadie lo ha mencionado, pero ¿tiene la opción de cambiar la parte principal y ejecutar un conjunto de pruebas que tenga todas las subclases de BaseTest?
kon psych

Respuestas:


154

Utilice la herencia múltiple, por lo que su clase con pruebas comunes no hereda de TestCase.

import unittest

class CommonTests(object):
    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(unittest.TestCase, CommonTests):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(unittest.TestCase, CommonTests):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

1
Esa es la solución más elegante hasta ahora.
Thierry Lam

27
Este método solo funciona para los métodos setUp y tearDown si invierte el orden de las clases base. Debido a que los métodos se definen en unittest.TestCase y no llaman a super (), entonces cualquier método setUp y tearDown en CommonTests debe ser el primero en el MRO, o no se llamarán en absoluto.
Ian Clelland

32
Solo para aclarar el comentario de Ian Clelland para que sea más claro para personas como yo: si agrega setUpy tearDownmétodos a la CommonTestsclase, y desea que se los llame para cada prueba en clases derivadas, debe invertir el orden de las clases base, por lo que será: class SubTest1(CommonTests, unittest.TestCase).
Dennis Golomazov

66
No soy realmente un fanático de este enfoque. Esto establece un contrato en el código que las clases deben heredar de ambos unittest.TestCase y CommonTests . Creo que el setUpClasssiguiente método es el mejor y es menos propenso a errores humanos. O eso, o envolver la clase BaseTest en una clase contenedor que es un poco más hacky pero evita el mensaje de omisión en la impresión de ejecución de prueba.
David Sanders

10
El problema con este es que pylint tiene un ajuste porque CommonTestsinvoca métodos que no existen en esa clase.
MadScientist

146

No uses herencia múltiple, te morderá más tarde .

En cambio, puede mover su clase base al módulo separado o envolverla con la clase en blanco:

class BaseTestCases:

    class BaseTest(unittest.TestCase):

        def testCommon(self):
            print('Calling BaseTest:testCommon')
            value = 5
            self.assertEqual(value, 5)


class SubTest1(BaseTestCases.BaseTest):

    def testSub1(self):
        print('Calling SubTest1:testSub1')
        sub = 3
        self.assertEqual(sub, 3)


class SubTest2(BaseTestCases.BaseTest):

    def testSub2(self):
        print('Calling SubTest2:testSub2')
        sub = 4
        self.assertEqual(sub, 4)

if __name__ == '__main__':
    unittest.main()

La salida:

Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

66
Este es mi favorito. Es el medio menos hacky y no interfiere con los métodos de anulación, no altera el MRO y me permite definir setUp, setUpClass, etc. en la clase base.
Hannes

66
En serio, no lo entiendo (¿de dónde viene la magia?), Pero es la mejor solución para mí :) Viniendo de Java, odio la herencia múltiple ...
Edouard Berthe

44
@Edouardb unittest solo ejecuta clases de nivel de módulo que heredan de TestCase. Pero BaseTest no es de nivel de módulo.
JoshB

Como una alternativa muy similar, podría definir el ABC dentro de una función sin argumentos que devuelve el ABC cuando se llama
Anakhand

34

Puede resolver este problema con un solo comando:

del(BaseTest)

Entonces el código se vería así:

import unittest

class BaseTest(unittest.TestCase):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

del(BaseTest)

if __name__ == '__main__':
    unittest.main()

3
BaseTest es un miembro del módulo mientras se está definiendo, por lo que está disponible para su uso como la clase base de SubTests. Justo antes de que se complete la definición, del () lo elimina como miembro, por lo que el marco de unittest no lo encontrará cuando busque subclases de TestCase en el módulo.
mhsmith

3
¡Esta es una respuesta increíble! Me gusta más que el de @MatthewMarshall porque en su solución, obtendrá errores de sintaxis de pylint, porque los self.assert*métodos no existen en un objeto estándar.
SimplyKnownAsG

1
No funciona si BaseTest está referenciado en cualquier otro lugar de la clase base o sus subclases, por ejemplo, cuando se llama a super () en la anulación de métodos: super( BaseTest, cls ).setUpClass( )
Hannes

1
@Hannes Al menos en Python 3, se BaseTestpuede hacer referencia a través de super(self.__class__, self)o solo super()en las subclases, aunque aparentemente no si heredara constructores . Tal vez también existe una alternativa "anónima" cuando la clase base necesita referenciarse a sí misma (no es que tenga idea de cuándo una clase necesita referenciarse a sí misma).
Stein

29

La respuesta de Matthew Marshall es excelente, pero requiere que heredes de dos clases en cada uno de tus casos de prueba, lo cual es propenso a errores. En cambio, uso esto (python> = 2.7):

class BaseTest(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        if cls is BaseTest:
            raise unittest.SkipTest("Skip BaseTest tests, it's a base class")
        super(BaseTest, cls).setUpClass()

3
Está muy bien. ¿Hay alguna forma de evitar tener que usar un salto? Para mí, los saltos son indeseables y se usan para indicar un problema en el plan de prueba actual (ya sea con el código o la prueba).
Zach Young

@ZacharyYoung No sé, tal vez otras respuestas puedan ayudar.
Dennis Golomazov

@ZacharyYoung He intentado solucionar este problema, mira mi respuesta.
simonzack

no está claro de inmediato qué es inherentemente propenso a errores al heredar de dos clases
jwg

@jwg vea los comentarios a la respuesta aceptada :) Debe heredar cada una de sus clases de prueba de las dos clases base; necesita preservar el orden correcto de ellos; Si desea agregar otra clase de prueba base, también necesitaría heredar de ella. Los mixins no tienen nada de malo, pero en este caso pueden reemplazarse con un simple salto.
Dennis Golomazov

7

¿Qué estás intentando lograr? Si tiene un código de prueba común (aserciones, pruebas de plantilla, etc.), colóquelos en métodos que no tengan el prefijo testpara unittestque no los cargue.

import unittest

class CommonTests(unittest.TestCase):
      def common_assertion(self, foo, bar, baz):
          # whatever common code
          self.assertEqual(foo(bar), baz)

class BaseTest(CommonTests):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(CommonTests):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)

class SubTest2(CommonTests):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

1
Según su sugerencia, ¿common_assertion () aún se ejecutará automáticamente al probar las subclases?
Stewart

@ Stewart No, no lo haría. La configuración predeterminada es ejecutar solo métodos que comiencen con "prueba".
CS

6

La respuesta de Matthew es la que necesitaba usar ya que todavía estoy en 2.5. Pero a partir de 2.7 puede usar el decorador @ unittest.skip () en cualquier método de prueba que desee omitir.

http://docs.python.org/library/unittest.html#skipping-tests-and-expected-failures

Tendrá que implementar su propio decorador de omisión para verificar el tipo base. No he usado esta función antes, pero desde la parte superior de mi cabeza puedes usar BaseTest como un tipo de marcador para condicionar el salto:

def skipBaseTest(obj):
    if type(obj) is BaseTest:
        return unittest.skip("BaseTest tests skipped")
    return lambda func: func

6

Una forma en que he pensado en resolver esto es ocultando los métodos de prueba si se usa la clase base. De esta forma, las pruebas no se omiten, por lo que los resultados pueden ser verdes en lugar de amarillos en muchas herramientas de informes de pruebas.

En comparación con el método mixin, ide's como PyCharm no se quejará de que faltan métodos de prueba unitaria en la clase base.

Si una clase base hereda de esta clase, deberá anular los métodos setUpClassy tearDownClass.

class BaseTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls._test_methods = []
        if cls is BaseTest:
            for name in dir(cls):
                if name.startswith('test') and callable(getattr(cls, name)):
                    cls._test_methods.append((name, getattr(cls, name)))
                    setattr(cls, name, lambda self: None)

    @classmethod
    def tearDownClass(cls):
        if cls is BaseTest:
            for name, method in cls._test_methods:
                setattr(cls, name, method)
            cls._test_methods = []

5

Puede agregar la __test_ = Falseclase BaseTest, pero si la agrega, tenga en cuenta que debe agregar las __test__ = Trueclases derivadas para poder ejecutar las pruebas.

import unittest

class BaseTest(unittest.TestCase):
    __test__ = False

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):
    __test__ = True

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):
    __test__ = True

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

Esta solución no funciona con el propio descubrimiento / prueba de prueba de unittest. (Creo que requiere el uso de un corredor de prueba alternativo, como la nariz.)
medmunds hace

4

Otra opción es no ejecutar

unittest.main()

En lugar de eso puedes usar

suite = unittest.TestLoader().loadTestsFromTestCase(TestClass)
unittest.TextTestRunner(verbosity=2).run(suite)

Entonces solo ejecutas las pruebas en la clase TestClass


Esta es la solución menos hacky. En lugar de modificar lo que se unittest.main()acumula en el conjunto predeterminado, forma un conjunto explícito y ejecuta sus pruebas.
zgoda

1

Hice casi lo mismo que @Vladim P. ( https://stackoverflow.com/a/25695512/2451329 ) pero ligeramente modificado:

import unittest2


from some_module import func1, func2


def make_base_class(func):

    class Base(unittest2.TestCase):

        def test_common1(self):
            print("in test_common1")
            self.assertTrue(func())

        def test_common2(self):
            print("in test_common1")
            self.assertFalse(func(42))

    return Base



class A(make_base_class(func1)):
    pass


class B(make_base_class(func2)):

    def test_func2_with_no_arg_return_bar(self):
        self.assertEqual("bar", func2())

y ahí vamos


1

A partir de Python 3.2, puede agregar una función test_loader a un módulo para controlar qué pruebas (si las hay) encuentra el mecanismo de descubrimiento de pruebas.

Por ejemplo, lo siguiente solo cargará los carteles originales SubTest1y SubTest2los casos de prueba, ignorando Base:

def load_tests(loader, standard_tests, pattern):
    suite = TestSuite()
    suite.addTests([SubTest1, SubTest2])
    return suite

Debería ser posible realizar una iteración standard_tests(que TestSuitecontiene las pruebas que encontró el cargador predeterminado) y copiar todo, pero Baseen su suitelugar, pero la naturaleza anidada TestSuite.__iter__hace que sea mucho más complicado.


0

Simplemente cambie el nombre del método testCommon a otra cosa. Unittest (por lo general) omite cualquier cosa que no tenga "prueba".

Rápido y sencillo

  import unittest

  class BaseTest(unittest.TestCase):

   def methodCommon(self):
       print 'Calling BaseTest:testCommon'
       value = 5
       self.assertEquals(value, 5)

  class SubTest1(BaseTest):

      def testSub1(self):
          print 'Calling SubTest1:testSub1'
          sub = 3
          self.assertEquals(sub, 3)


  class SubTest2(BaseTest):

      def testSub2(self):
          print 'Calling SubTest2:testSub2'
          sub = 4
          self.assertEquals(sub, 4)

  if __name__ == '__main__':
      unittest.main()`

2
Esto tendría el resultado de no ejecutar la prueba methodCommon en ninguno de los SubTests.
Pepper Lebeck-Jobe

0

Así que este es un hilo viejo, pero hoy me encontré con este problema y pensé en mi propio truco. Utiliza un decorador que convierte los valores de las funciones en Ninguno cuando se accede a través de la clase base. No necesita preocuparse por la configuración y la clase de configuración porque si la clase base no tiene pruebas, no se ejecutarán.

import types
import unittest


class FunctionValueOverride(object):
    def __init__(self, cls, default, override=None):
        self.cls = cls
        self.default = default
        self.override = override

    def __get__(self, obj, klass):
        if klass == self.cls:
            return self.override
        else:
            if obj:
                return types.MethodType(self.default, obj)
            else:
                return self.default


def fixture(cls):
    for t in vars(cls):
        if not callable(getattr(cls, t)) or t[:4] != "test":
            continue
        setattr(cls, t, FunctionValueOverride(cls, getattr(cls, t)))
    return cls


@fixture
class BaseTest(unittest.TestCase):
    def testCommon(self):
        print('Calling BaseTest:testCommon')
        value = 5
        self.assertEqual(value, 5)


class SubTest1(BaseTest):
    def testSub1(self):
        print('Calling SubTest1:testSub1')
        sub = 3
        self.assertEqual(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print('Calling SubTest2:testSub2')
        sub = 4
        self.assertEqual(sub, 4)

if __name__ == '__main__':
    unittest.main()

0

Aquí hay una solución que usa solo características documentadas de unittest y evita tener un estado de "omisión" en los resultados de la prueba:

class BaseTest(unittest.TestCase):

    def __init__(self, methodName='runTest'):
        if self.__class__ is BaseTest:
            # don't run these tests in the abstract base implementation
            methodName = 'runNoTestsInBaseClass'
        super().__init__(methodName)

    def runNoTestsInBaseClass(self):
        pass

    def testCommon(self):
        # everything else as in the original question

Cómo funciona: según la unittest.TestCasedocumentación , "Cada instancia de TestCase ejecutará un único método base: el método llamado methodName". El "runTests" predeterminado ejecuta todos los métodos de prueba * en la clase, así es como funcionan normalmente las instancias de TestCase. Pero cuando se ejecuta en la propia clase base abstracta, simplemente puede anular ese comportamiento con un método que no hace nada.

Un efecto secundario es que su recuento de pruebas aumentará en uno: la "prueba" runNoTestsInBaseClass se cuenta como una prueba exitosa cuando se ejecuta en BaseClass.

(Esto también funciona en Python 2.7, si todavía está en eso. Simplemente cambie super()a super(BaseTest, self)).


-2

Cambie el nombre del método BaseTest para configurar:

class BaseTest(unittest.TestCase):
    def setUp(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)


class SubTest1(BaseTest):
    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):
    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

Salida:

Ran 2 pruebas en 0.000s

Calling BaseTest: testCommon Calling
SubTest1: testSub1 Calling
BaseTest: testCommon Calling
SubTest2: testSub2

De la documentación :

TestCase.setUp ()
Método llamado para preparar el dispositivo de prueba. Esto se llama inmediatamente antes de llamar al método de prueba; Cualquier excepción planteada por este método se considerará un error en lugar de una falla de prueba. La implementación predeterminada no hace nada.


Eso funcionaría, ¿y si tengo n testCommon, debería colocarlos a todos setUp?
Thierry Lam

1
Sí, debe poner todo el código que no es un caso de prueba real en la configuración.
Brian R. Bondy

Pero si una subclase tiene más de un test...método, setUpse ejecuta una y otra vez, una vez por cada método; ¡así que NO es una buena idea poner pruebas allí!
Alex Martelli el

No estoy realmente seguro de lo que OP quería en términos de cuándo se ejecuta en un escenario más complejo.
Brian R. Bondy
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.