Prueba unitaria de Python: ¿opuesto a afirmar elevaciones?

374

Quiero escribir una prueba para establecer que una excepción no se genera en una circunstancia determinada.

Es sencillo a prueba si una excepción se levantó ...

sInvalidPath=AlwaysSuppliesAnInvalidPath()
self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath) 

... pero cómo puedes hacer lo contrario .

Algo como esto, lo que busco ...

sValidPath=AlwaysSuppliesAValidPath()
self.assertNotRaises(PathIsNotAValidOne, MyObject, sValidPath) 
glaucon
fuente
66
Siempre puedes hacer lo que sea que funcione en la prueba. Si genera un error, aparecerá (contado como un error, en lugar de un error). Por supuesto, eso supone que no genera ningún error, en lugar de solo un tipo definido de error. Aparte de eso, supongo que tendrías que escribir el tuyo.
Thomas K
Resulta que, de hecho, puede implementar un assertNotRaisesmétodo que comparta el 90% de su código / comportamiento con assertRaisesaproximadamente ~ 30 líneas de código. Vea mi respuesta a continuación para más detalles.
tel
Quiero esto para poder comparar dos funciones con el hypothesisfin de asegurarme de que produzcan la misma salida para todo tipo de entrada, sin tener en cuenta los casos en que el original genera una excepción. assume(func(a))no funciona porque la salida puede ser una matriz con un valor de verdad ambiguo. Así que solo quiero llamar a una función y obtenerla Truesi no falla. assume(func(a) is not None)funciona, supongo
endolith

Respuestas:

394
def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")
DGH
fuente
32
@hiwaylon - No, de hecho, esta es la solución correcta. La solución propuesta por el usuario 9876 es conceptualmente defectuosa: si realiza una prueba para la no elevación de say ValueError, pero en ValueErrorsu lugar se genera, su prueba debe salir con una condición de falla, no una de error. Por otro lado, si al ejecutar el mismo código generarías un KeyError, eso sería un error, no un error. En python, de manera diferente a otros lenguajes, las excepciones se usan de manera rutinaria para el flujo de control, por eso tenemos la except <ExceptionName>sintaxis. En ese sentido, la solución de user9876 es simplemente incorrecta.
mac
@mac - ¿Es esta también una solución correcta? stackoverflow.com/a/4711722/6648326
MasterJoe2
Este tiene el desafortunado efecto de mostrar <100% de cobertura (la excepción nunca sucederá) para las pruebas.
Shay
3
@ Shay, OMI, siempre debe excluir los archivos de prueba de los informes de cobertura (ya que casi siempre se ejecutan al 100% por definición, estaría inflando artificialmente los informes)
Salsa BBQ original
@ original-bbq-sauce, eso no me dejaría abierto a pruebas omitidas involuntariamente. Por ejemplo, error tipográfico en el nombre de la prueba (ttst_function), configuración de ejecución incorrecta en pycharm, etc.
Shay
67

Hola, quiero escribir una prueba para establecer que no se genera una excepción en una circunstancia determinada.

Esa es la suposición predeterminada: no se generan excepciones.

Si no dice nada más, eso se supone en cada prueba.

No tienes que escribir ninguna afirmación para eso.

S.Lott
fuente
77
@IndradhanushGupta Bueno, la respuesta aceptada hace que la prueba sea más pitónica que esta. Explícito es mejor que implícito.
0xc0de
17
Ningún otro comentarista ha señalado por qué esta respuesta es incorrecta, aunque es la misma razón por la que la respuesta del usuario9876 es incorrecta: las fallas y los errores son cosas diferentes en el código de prueba. Si su función fuera a lanzar una excepción durante una prueba de que no se afirma, el marco de pruebas que trataría como un error , en lugar de un fracaso a no afirma.
coredumperror
@ CoreDumpError Entiendo la diferencia entre una falla y un error, pero ¿no te obligaría a rodear cada prueba con una construcción de prueba / excepción? ¿O recomendaría hacer eso solo para las pruebas que explícitamente generan una excepción bajo alguna condición (lo que básicamente significa que se espera la excepción ).
federicojasson
44
@federicojasson Respondiste bastante bien a tu propia pregunta en esa segunda oración. Los errores frente a fallas en las pruebas pueden describirse sucintamente como "bloqueos inesperados" frente a "comportamiento involuntario", respectivamente. Desea que sus pruebas muestren un error cuando su función falla, pero no cuando se produce una excepción que sabe que arrojará dado que ciertas entradas se lanzan cuando se le dan diferentes entradas.
coredumperror
52

Solo llama a la función. Si genera una excepción, el marco de prueba de la unidad marcará esto como un error. Es posible que desee agregar un comentario, por ejemplo:

sValidPath=AlwaysSuppliesAValidPath()
# Check PathIsNotAValidOne not thrown
MyObject(sValidPath)
user9876
fuente
35
Las fallas y los errores son conceptualmente diferentes. Además, dado que en Python las excepciones se usan rutinariamente para el flujo de control, esto hará que sea muy difícil de entender de un vistazo (= sin explorar el código de prueba) si ha roto su lógica o su código ...
mac
1
O tu prueba pasa o no pasa. Si no pasa, tendrás que ir a arreglarlo. Si se informa como un "error" o un "error" es irrelevante. Hay una diferencia: con mi respuesta, verá el seguimiento de la pila para que pueda ver dónde se arrojó PathIsNotAValidOne; con la respuesta aceptada no tendrá esa información, por lo que la depuración será más difícil. (Suponiendo Py2; no estoy seguro si Py3 es mejor en esto).
user9876
19
@ user9876 - No. Las condiciones de salida de la prueba son 3 (pasa / no pasa / error), no 2 como parece creer erróneamente. La diferencia entre errores y fallas es sustancial y tratarlos como si fueran lo mismo es simplemente una programación deficiente. Si no me cree, solo mire a su alrededor cómo funcionan los corredores de prueba y qué árboles de decisión implementan para fallas y errores. Un buen punto de partida para python es el xfaildecorador en pytest.
mac
44
Supongo que depende de cómo uses las pruebas unitarias. La forma en que mi equipo usa las pruebas unitarias, tienen que pasar todas. (Programación ágil, con una máquina de integración continua que ejecuta todas las pruebas unitarias). Sé que el caso de prueba puede informar "pasar", "fallar" o "error". Pero a un alto nivel, lo que realmente le importa a mi equipo es "¿pasan todas las pruebas unitarias?" (es decir, "¿Jenkins es verde?"). Entonces, para mi equipo, no hay una diferencia práctica entre "fallar" y "error". Es posible que tenga requisitos diferentes si utiliza sus pruebas unitarias de una manera diferente.
user9876
1
@ user9876 la diferencia entre 'error' y 'error' es la diferencia entre "mi afirmación falló" y "mi prueba ni siquiera llega a la afirmación". Eso, para mí, es una distinción útil durante la fijación de pruebas, pero supongo, como usted dice, no para todos.
CS
14

Soy el póster original y acepté la respuesta anterior de DGH sin haberla usado primero en el código.

Una vez que lo utilicé, me di cuenta de que necesitaba un pequeño ajuste para realmente hacer lo que tenía que hacer (para ser justos con DGH, ¡él / ella dijo "o algo similar"!).

Pensé que valía la pena publicar el ajuste aquí para beneficio de otros:

    try:
        a = Application("abcdef", "")
    except pySourceAidExceptions.PathIsNotAValidOne:
        pass
    except:
        self.assertTrue(False)

Lo que intentaba hacer aquí era asegurarme de que si se intentaba crear una instancia de un objeto Aplicación con un segundo argumento de espacios, se generaría pySourceAidExceptions.PathIsNotAValidOne.

Creo que usar el código anterior (basado en gran medida en la respuesta de DGH) hará eso.

glaucon
fuente
2
Como está aclarando su pregunta y no la responde, debería haberla editado (no haberla respondido). Consulte mi respuesta a continuación.
hiwaylon
13
Parece ser todo lo contrario al problema original. self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath)debería hacer el trabajo en este caso.
Antony Hatchkins
8

Puede definir assertNotRaisesreutilizando aproximadamente el 90% de la implementación original de assertRaisesen el unittestmódulo. Con este enfoque, terminas con un assertNotRaisesmétodo que, aparte de su condición de falla inversa, se comporta de manera idéntica assertRaises.

TLDR y demostración en vivo

Resulta sorprendentemente fácil agregarle un assertNotRaisesmétodo unittest.TestCase(me tomó alrededor de 4 veces más tiempo escribir esta respuesta que el código). Aquí hay una demostración en vivo del assertNotRaisesmétodo en acción . Al igual queassertRaises , puede pasar un invocable y argumentos a assertNotRaises, o puede usarlo en una withdeclaración. La demostración en vivo incluye una prueba de casos que demuestra que assertNotRaisesfunciona según lo previsto.

Detalles

La implementación de assertRaisesin unittestes bastante complicada, pero con un poco de subclases inteligentes puede anular e invertir su condición de falla.

assertRaiseses un método corto que básicamente solo crea una instancia de la unittest.case._AssertRaisesContextclase y la devuelve (consulte su definición en el unittest.casemódulo). Puede definir su propia _AssertNotRaisesContextclase subclasificando _AssertRaisesContexty anulando su __exit__método:

import traceback
from unittest.case import _AssertRaisesContext

class _AssertNotRaisesContext(_AssertRaisesContext):
    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is not None:
            self.exception = exc_value.with_traceback(None)

            try:
                exc_name = self.expected.__name__
            except AttributeError:
                exc_name = str(self.expected)

            if self.obj_name:
                self._raiseFailure("{} raised by {}".format(exc_name,
                    self.obj_name))
            else:
                self._raiseFailure("{} raised".format(exc_name))

        else:
            traceback.clear_frames(tb)

        return True

Normalmente define clases de casos de prueba haciendo que hereden de TestCase. Si en cambio heredas de una subclase MyTestCase:

class MyTestCase(unittest.TestCase):
    def assertNotRaises(self, expected_exception, *args, **kwargs):
        context = _AssertNotRaisesContext(expected_exception, self)
        try:
            return context.handle('assertNotRaises', args, kwargs)
        finally:
            context = None

Todos sus casos de prueba ahora tendrán el assertNotRaisesmétodo disponible para ellos.

tel
fuente
¿Dónde está tu tracebacken su elsedeclaración viene?
NOh
1
@NOhs Hubo una falta import. Su fijo
tel
2
def _assertNotRaises(self, exception, obj, attr):                                                                                                                              
     try:                                                                                                                                                                       
         result = getattr(obj, attr)                                                                                                                                            
         if hasattr(result, '__call__'):                                                                                                                                        
             result()                                                                                                                                                           
     except Exception as e:                                                                                                                                                     
         if isinstance(e, exception):                                                                                                                                           
            raise AssertionError('{}.{} raises {}.'.format(obj, attr, exception)) 

podría modificarse si necesita aceptar parámetros.

llamar como

self._assertNotRaises(IndexError, array, 'sort')
znotdead
fuente
1

Lo he encontrado útil para parchear mono de la unittestsiguiente manera:

def assertMayRaise(self, exception, expr):
  if exception is None:
    try:
      expr()
    except:
      info = sys.exc_info()
      self.fail('%s raised' % repr(info[0]))
  else:
    self.assertRaises(exception, expr)

unittest.TestCase.assertMayRaise = assertMayRaise

Esto aclara la intención al probar la ausencia de una excepción:

self.assertMayRaise(None, does_not_raise)

Esto también simplifica las pruebas en un bucle, que a menudo me encuentro haciendo:

# ValueError is raised only for op(x,x), op(y,y) and op(z,z).
for i,(a,b) in enumerate(itertools.product([x,y,z], [x,y,z])):
  self.assertMayRaise(None if i%4 else ValueError, lambda: op(a, b))
AndyJost
fuente
¿Qué es un parche de mono?
ScottMcC
1
Ver en.wikipedia.org/wiki/Monkey_patch . Después de agregar assertMayRaisea unittest.TestSuiteusted, puede fingir que es parte de la unittestbiblioteca.
AndyJost
0

Si pasa una clase de excepción a assertRaises(), se proporciona un administrador de contexto. Esto puede mejorar la legibilidad de sus pruebas:

# raise exception if Application created with bad data
with self.assertRaises(pySourceAidExceptions.PathIsNotAValidOne):
    application = Application("abcdef", "")

Esto le permite probar casos de error en su código.

En este caso, está probando que PathIsNotAValidOnese genera cuando pasa parámetros no válidos al constructor de la aplicación.

hiwaylon
fuente
1
No, esto solo fallará si la excepción no se genera dentro del bloque del administrador de contexto. Se puede probar fácilmente con 'with self.assertRaises (TypeError): raise TypeError', que pasa.
Matthew Trevor
@MatthewTrevor Buena llamada. Como recuerdo, en lugar de que el código de prueba se ejecute correctamente, es decir, no se eleva, sugería casos de error de prueba. He editado la respuesta en consecuencia. Espero poder ganar un +1 para salir de la red. :)
hiwaylon
Tenga en cuenta que esto también es Python 2.7 y posterior: docs.python.org/2/library/…
qneill
0

puedes intentarlo así. try: self.assertRaises (None, function, arg1, arg2) excepto: pase si no coloca el código dentro del bloque try, a través de la excepción 'AssertionError: None not raised "y el caso de prueba fallará. El caso de prueba se pasará si se coloca dentro del bloque try, que es el comportamiento esperado.

lalit
fuente
0

Una forma directa de garantizar que el objeto se inicialice sin ningún error es probar la instancia de tipo del objeto.

Aquí hay un ejemplo :

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))
anroyus
fuente