python - tipos - ¿Cómo deberíamos probar las excepciones con la nariz?
excepciones en python (6)
No sé por qué no está aquí todavía, pero existe una manera más:
import unittest
class TestCase(unittest.TestCase):
def testKeyError(self):
d = dict()
with self.assertRaises(KeyError):
d[1]
Estoy probando excepciones con la nariz. Aquí hay un ejemplo:
def testDeleteUserUserNotFound(self):
"Test exception is raised when trying to delete non-existent users"
try:
self.client.deleteUser(''10000001-0000-0000-1000-100000000000'')
# make nose fail here
except UserNotFoundException:
assert True
El aserto se ejecuta si se genera la excepción, pero si no se produce una excepción, no se ejecutará.
¿Hay algo que pueda poner en la línea comentada de arriba para que, si no se produce una excepción, informe un error?
No sé qué es la nariz, pero ha intentado usar ''else'' después de la cláusula de excepción. Es decir
else:
assert False
Recomiendo usar assert_raises
y assert_raises_regexp
de nose.tools
, que duplican el comportamiento de assertRaises
y assertRaisesRegexp
de unittest.TestCase
. Estas permiten el uso de la misma funcionalidad proporcionada por unittest.TestCase
en las suites de prueba que en realidad no usan la clase unittest.TestCase
.
Me parece que @raises
es un instrumento demasiado contundente. Aquí está el código que ilustra el problema:
from nose.tools import *
something = ["aaa", "bbb"]
def foo(x, source=None):
if source is None:
source = something
return source[x]
# This is fine
@raises(IndexError)
def test1():
foo(3)
# This is fine. The expected error does not happen because we made
# a mistake in the test or in the code. The failure indicates we made
# a mistake.
@raises(IndexError)
def test2():
foo(1)
# This passes for the wrong reasons.
@raises(IndexError)
def test3():
source = something[2] # This is the line that raises the exception.
foo(10, source) # This is not tested.
# When we use assert_raises, we can isolate the line where we expect
# the failure. This causes an error due to the exception raised in
# the first line of the function.
def test4():
source = something[2]
with assert_raises(IndexError):
foo(10, source)
test3
pasa, pero no porque foo
haya generado la excepción que esperábamos, sino porque el código que configura los datos para que foo
falle con la misma excepción. test4
muestra cómo se puede escribir la prueba utilizando assert_raises
para probar realmente lo que queremos hacer. El problema en la primera línea causará que Nose informe un error y luego podemos reescribir la prueba para que esa línea finalmente pueda probar lo que significamos probar.
@raises
no permite probar el mensaje asociado con la excepción. Cuando ValueError
, solo para tomar un ejemplo, usualmente quiero subirlo con un mensaje informativo. Aquí hay un ejemplo:
def bar(arg):
if arg: # This is incorrect code.
raise ValueError("arg should be higher than 3")
if arg >= 10:
raise ValueError("arg should be less than 10")
# We don''t know which of the possible `raise` statements was reached.
@raises(ValueError)
def test5():
bar(10)
# Yes, we''re getting an exception but with the wrong value: bug found!
def test6():
with assert_raises_regexp(ValueError, "arg should be less than 10"):
bar(10)
test5
que usa @raises
pasará, pero pasará por la razón equivocada. test6
realiza una prueba más test6
que revela que el ValueError
generado no fue el que queríamos.
Utilice assert_raises
:
from nose.tools import assert_raises
our_method = self.client.deleteUser
arg1 = ''10000001-0000-0000-1000-100000000000''
expected_exception = UserNotFoundException
assert_raises(expected_exception, our_method, arg1)
Usar la prueba y captura en sus pruebas parece ser una mala práctica (la mayoría de los casos).
No hay documentación específica en la nariz porque básicamente es solo una envoltura alrededor de assertRaises (ref. ¿Cómo usar assert_raises de nose? )
nose proporciona herramientas para probar excepciones (como hace unittest). Pruebe este ejemplo (y lea sobre las otras herramientas en Nose Testing Tools
from nose.tools import *
l = []
d = dict()
@raises(Exception)
def test_Exception1():
''''''this test should pass''''''
l.pop()
@raises(KeyError)
def test_Exception2():
''''''this test should pass''''''
d[1]
@raises(KeyError)
def test_Exception3():
''''''this test should fail (IndexError raised but KeyError was expected)''''''
l.pop()
def test_Exception4():
''''''this test should fail with KeyError''''''
d[1]
Creo que esta es la forma correcta que estaba buscando porque le permite ser específico acerca de las excepciones que espera o desea. Entonces realmente provocas el error para ver que genera la excepción correcta. Y luego dejas que la nariz evalúe el resultado. (¡Ponga la menor lógica posible en las pruebas unitarias!)
def testDeleteUserUserNotFound(self):
"Test exception is raised when trying to delete non-existent users"
try:
self.client.deleteUser(''10000001-0000-0000-1000-100000000000'')
assert False # <---
except UserNotFoundException:
assert True
La semántica de try
/ except
implica que el flujo de ejecución deja el bloque try
en una excepción, por lo que assert False
no se ejecutará si se produce una excepción. Además, la ejecución no volverá a entrar en el bloque de try
una vez que el bloque de except
haya terminado de ejecutarse, por lo que no debería tener problemas.
↓
(statements)
↓ exception
(try) ↚──────────→ (except)
↓ │
(statements) ←───────────┘
↓