Cómo usar pytest para verificar que NO se genera un error

83

Supongamos que tenemos algo así:

import py, pytest

ERROR1 = ' --- Error : value < 5! ---'
ERROR2 = ' --- Error : value > 10! ---'

class MyError(Exception):
    def __init__(self, m):
        self.m = m

    def __str__(self):
        return self.m

def foo(i):
    if i < 5:
        raise MyError(ERROR1)
    elif i > 10:
        raise MyError(ERROR2)
    return i


# ---------------------- TESTS -------------------------
def test_foo1():
    with pytest.raises(MyError) as e:
        foo(3)
    assert ERROR1 in str(e)

def test_foo2():
    with pytest.raises(MyError) as e:
        foo(11)
    assert ERROR2 in str(e)

def test_foo3():
        ....
        foo(7)
         ....

P: ¿Cómo puedo hacer test_foo3 () para probar que no se genera MyError? Es obvio que podría probar:

def test_foo3():
    assert foo(7) == 7

pero quiero probar eso a través de pytest.raises (). ¿Es posible de alguna manera? Por ejemplo: en un caso, esa función "foo" no tiene ningún valor de retorno,

def foo(i):
    if i < 5:
        raise MyError(ERROR1)
    elif i > 10:
        raise MyError(ERROR2)

podría tener sentido probar de esta manera, en mi humilde opinión.

paraklet
fuente
Parece que está buscando un problema, la prueba de código foo(7)está bien. Obtendrá el mensaje correcto y será más fácil de depurar con toda la salida de pytest. La sugerencia que forzó de @Faruk ( 'Unexpected error...') no dice nada sobre el error y quedará atascado. Lo único que puede hacer para mejorarlo es expresar su intención test_foo3_works_on_integers_within_range().
dhill

Respuestas:

125

Una prueba fallará si genera algún tipo de excepción inesperada. Puede simplemente invocar foo (7) y habrá probado que no se genera MyError. Entonces, lo siguiente será suficiente:

def test_foo3():
    foo(7)

Si desea ser explícito y escribir una declaración de aserción para esto, puede hacer:

def test_foo3():
    try:
        foo(7)
    except MyError:
        pytest.fail("Unexpected MyError ..")
Faruk Sahin
fuente
3
Gracias, funciona, pero parece más un truco que una solución limpia. Por ejemplo, la prueba de foo (4) fallará, pero no debido a un error de afirmación.
paraklet
la prueba de foo (4) fallará porque arrojará una excepción que no se esperaba. Otra forma sería envolverlo en un bloque try catch y fallar con un mensaje específico. Actualizaré mi respuesta.
Faruk Sahin
1
Si tiene muchos casos como este, podría ser útil escribir eso en una función simple: `` `def not_raises (error_class, func, * args, ** kwargs): ...` `` O puede escribir un con un enfoque similar al de Pytest. Si lo hace, le sugiero que escriba un PR con esto para beneficiar a todos. :) (El repositorio está en bitbucket ).
Bruno Oliveira
6
@paraklet - el lema principal de pytest es "pruebas sin repetición" . Está muy en el espíritu de pytest permitirle escribir pruebas como en el primer ejemplo de Faruk, mientras que pytest maneja los detalles por usted. Para mí, el primer ejemplo es la "solución limpia" y el segundo parece innecesariamente detallado.
Nick Chammas
21

Sobre la base de lo que Oisin mencionó.

Puede hacer una not_raisesfunción simple que actúe de manera similar a la de pytest raises:

from contextlib import contextmanager

@contextmanager
def not_raises(exception):
  try:
    yield
  except exception:
    raise pytest.fail("DID RAISE {0}".format(exception))

Esto está bien si desea raisestener una contraparte y, por lo tanto, sus pruebas sean más legibles. Sin embargo, en esencia, no necesita nada más que ejecutar el bloque de código que desea probar en su propia línea; pytest fallará de todos modos tan pronto como ese bloque genere un error.

Pithikos
fuente
1
Ojalá esto estuviera integrado en py.test; haría que las pruebas fueran mucho más legibles en algunos casos. Especialmente junto con @pytest.mark.parametrize.
Arel
¡Aprecio mucho el sentido de legibilidad del código en este enfoque!
GrazingScientist
6

Tenía curiosidad por ver si un not_raises funcionaría. Una prueba rápida de esto es (test_notraises.py):

from contextlib import contextmanager

@contextmanager
def not_raises(ExpectedException):
    try:
        yield

    except ExpectedException, err:
        raise AssertionError(
            "Did raise exception {0} when it should not!".format(
                repr(ExpectedException)
            )
        )

    except Exception, err:
        raise AssertionError(
            "An unexpected exception {0} raised.".format(repr(err))
        )

def good_func():
    print "hello"


def bad_func():
    raise ValueError("BOOM!")


def ugly_func():
    raise IndexError("UNEXPECTED BOOM!")


def test_ok():
    with not_raises(ValueError):
        good_func()


def test_bad():
    with not_raises(ValueError):
        bad_func()


def test_ugly():
    with not_raises(ValueError):
        ugly_func()

Parece funcionar. Sin embargo, no estoy seguro de si realmente se lee bien en la prueba.

Oisin
fuente