No se puede capturar la excepción simulada porque no hereda BaseException

Estoy trabajando en un proyecto que implica conectarse a un servidor remoto, esperar una respuesta y luego realizar acciones basadas en esa respuesta. Capturamos un par de excepciones diferentes y nos comportamos de manera diferente según la excepción que se detecte. Por ejemplo:

def myMethod(address, timeout=20): try: response = requests.head(address, timeout=timeout) except requests.exceptions.Timeout: # do something special except requests.exceptions.ConnectionError: # do something special except requests.exceptions.HTTPError: # do something special else: if response.status_code != requests.codes.ok: # do something special return successfulConnection.SUCCESS 

Para probar esto, hemos escrito una prueba como la siguiente

 class TestMyMethod(unittest.TestCase): def test_good_connection(self): config = { 'head.return_value': type('MockResponse', (), {'status_code': requests.codes.ok}), 'codes.ok': requests.codes.ok } with mock.patch('path.to.my.package.requests', **config): self.assertEqual( mypackage.myMethod('some_address', mypackage.successfulConnection.SUCCESS ) def test_bad_connection(self): config = { 'head.side_effect': requests.exceptions.ConnectionError, 'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError } with mock.patch('path.to.my.package.requests', **config): self.assertEqual( mypackage.myMethod('some_address', mypackage.successfulConnection.FAILURE ) 

Si ejecuto la función directamente, todo sucede como se esperaba. Incluso probé agregando raise requests.exceptions.ConnectionError a la cláusula try de la función. Pero cuando ejecuto mis pruebas unitarias, me sale

 ERROR: test_bad_connection (test.test_file.TestMyMethod) ---------------------------------------------------------------- Traceback (most recent call last): File "path/to/sourcefile", line ###, in myMethod respone = requests.head(address, timeout=timeout) File "path/to/unittest/mock", line 846, in __call__ return _mock_self.mock_call(*args, **kwargs) File "path/to/unittest/mock", line 901, in _mock_call raise effect my.package.requests.exceptions.ConnectionError During handling of the above exception, another exception occurred: Traceback (most recent call last): File "Path/to/my/test", line ##, in test_bad_connection mypackage.myMethod('some_address', File "Path/to/package", line ##, in myMethod except requests.exceptions.ConnectionError: TypeError: catching classes that do not inherit from BaseException is not allowed 

Intenté cambiar la excepción que estaba BaseException a BaseException y obtuve un error más o menos idéntico.

Ya he leído https://stackoverflow.com/a/18163759/3076272 , así que creo que debe ser un mal __del__ gancho en alguna parte, pero no estoy seguro de dónde buscarlo o de lo que puedo hacer en el tiempo medio También soy relativamente nuevo en unittest.mock.patch() así que es muy posible que también esté haciendo algo mal allí.

Este es un complemento de Fusion360, por lo que utiliza la versión empaquetada de Python 3.3 de Fusion 360, por lo que sé, es una versión de vainilla (es decir, no se filtran), pero no estoy seguro de eso.

Podría reproducir el error con un ejemplo mínimo:

foo.py:

 class MyError(Exception): pass class A: def inner(self): err = MyError("FOO") print(type(err)) raise err def outer(self): try: self.inner() except MyError as err: print ("catched ", err) return "OK" 

Prueba sin burla:

 class FooTest(unittest.TestCase): def test_inner(self): a = foo.A() self.assertRaises(foo.MyError, a.inner) def test_outer(self): a = foo.A() self.assertEquals("OK", a.outer()) 

Ok, todo está bien, ambos pasan la prueba

El problema viene con las burlas. Tan pronto como la clase MyError se burla, la cláusula de expect no puede detectar nada y aparece el mismo error que el ejemplo de la pregunta:

 class FooTest(unittest.TestCase): def test_inner(self): a = foo.A() self.assertRaises(foo.MyError, a.inner) def test_outer(self): with unittest.mock.patch('foo.MyError'): a = exc2.A() self.assertEquals("OK", a.outer()) 

Inmediatamente da:

 ERROR: test_outer (__main__.FooTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "...\foo.py", line 11, in outer self.inner() File "...\foo.py", line 8, in inner raise err TypeError: exceptions must derive from BaseException During handling of the above exception, another exception occurred: Traceback (most recent call last): File "", line 8, in test_outer File "...\foo.py", line 12, in outer except MyError as err: TypeError: catching classes that do not inherit from BaseException is not allowed 

Aquí obtengo un primer TypeError que no tenías, porque estoy generando un simulacro mientras forzaste una verdadera excepción con 'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError en la configuración. Pero el problema sigue siendo que la cláusula de except intenta atrapar una burla .

TL / DR: cuando usted se burla del paquete de requests completo, la cláusula except requests.exceptions.ConnectionError request.exceptions.ConnectionError intenta detectar un simulacro. Como el simulacro no es realmente una BaseException , causa el error.

La única solución que puedo imaginar es no burlarse de las requests completas, sino solo de las partes que no son excepciones. Debo admitir que no pude encontrar la forma de decir que se burlaba de todo, excepto de esto, pero en su ejemplo, solo necesita parchear las requests.head . Así que creo que esto debería funcionar:

 def test_bad_connection(self): with mock.patch('path.to.my.package.requests.head', side_effect=requests.exceptions.ConnectionError): self.assertEqual( mypackage.myMethod('some_address', mypackage.successfulConnection.FAILURE ) 

Es decir: solo parchea el método de la head con la excepción como efecto secundario.

Me encontré con el mismo problema al intentar simular sqlite3 (y encontré esta publicación mientras buscaba soluciones).

Lo que Serge dijo es correcto:

TL / DR: cuando usted se burla del paquete de solicitudes completo, la cláusula except request.exceptions.ConnectionError intenta detectar un simulacro. Como el simulacro no es realmente una BaseException, causa el error.

La única solución que puedo imaginar es no burlarse de las solicitudes completas, sino solo de las partes que no son excepciones. Debo admitir que no pude encontrar la manera de decir burlarme de todo, excepto de esto.

Mi solución fue burlarse de todo el módulo, luego establecer el atributo de simulacro para que la excepción fuera igual a la excepción en la clase real, efectivamente “desobedeciendo” la excepción. Por ejemplo, en mi caso:

 @mock.patch(MyClass.sqlite3) def test_connect_fail(self, mock_sqlite3): mock_sqlite3.connect.side_effect = sqlite3.OperationalError() mock_sqlite3.OperationalError = sqlite3.OperationalError self.assertRaises(sqlite3.OperationalError, MyClass, self.db_filename) 

Para las requests , puede asignar excepciones individualmente como esta:

  mock_requests.exceptions.ConnectionError = requests.exceptions.ConnectionError 

o hazlo para todas las requests excepciones como esta:

  mock_requests.exceptions = requests.exceptions 

No sé si esta es la forma “correcta” de hacerlo, pero hasta ahora parece funcionar para mí sin ningún problema.

Para aquellos de nosotros que necesitamos simular una excepción y no podemos hacerlo simplemente parcheando la head , aquí hay una solución fácil que reemplaza la excepción de destino con una vacía:

Digamos que tenemos una unidad genérica para probar, con una excepción que tenemos que haber burlado:

 # app/foo_file.py def test_me(): try: foo() return "No foo error happened" except CustomError: # <-- Mock me! return "The foo error was caught" 

Queremos CustomError pero como es una excepción, tenemos problemas si intentamos parchearlo como todo lo demás. Normalmente, una llamada a un patch reemplaza al objective con un MagicMock pero eso no funcionará aquí. Las simulaciones son ingeniosas, pero no se comportan como lo hacen las excepciones. En lugar de parchear con un simulacro, démosle una excepción de código auxiliar. Lo haremos en nuestro archivo de prueba.

 # app/test_foo_file.py from mock import patch # A do-nothing exception we are going to replace CustomError with class StubException(Exception): pass # Now apply it to our test @patch('app.foo_file.foo') @patch('app.foo_file.CustomError', new_callable=lambda: StubException) def test_foo(stub_exception, mock_foo): mock_foo.side_effect = stub_exception("Stub") # Raise our stub to be caught by CustomError assert test_me() == "The error was caught" # Success! 

Entonces, ¿qué pasa con la lambda ? El new_callable llama a lo que le demos y reemplaza el objective con el retorno de esa llamada. Si pasamos nuestra clase StubException directamente, llamará al constructor de la clase y parcheará nuestro objeto objective con una instancia de excepción en lugar de una clase que no es lo que queremos. Envolviéndolo con lambda , devuelve nuestra clase como pretendemos.

Una vez que se realiza la stub_exception parches, el objeto stub_exception (que es literalmente nuestra clase StubException ) se puede levantar y capturar como si fuera el CustomError . ¡Ordenado!

Me enfrenté a un problema similar al intentar burlarme del paquete sh . Si bien sh es muy útil, el hecho de que todos los métodos y las excepciones se definan dinámicamente hace que sea más difícil burlarse de ellos. Así que siguiendo la recomendación de la documentación :

 import unittest from unittest.mock import Mock, patch class MockSh(Mock): # error codes are defined dynamically in sh class ErrorReturnCode_32(BaseException): pass # could be any sh command def mount(self, *args): raise self.ErrorReturnCode_32 class MyTestCase(unittest.TestCase): mock_sh = MockSh() @patch('core.mount.sh', new=mock_sh) def test_mount(self): ... 

Acabo de encontrarme con el mismo problema cuando me burlo de la struct .

Me sale el error:

TypeError: las clases de captura que no heredan de BaseException no están permitidas

Al intentar capturar un struct.error elevado desde struct.unpack .

Descubrí que la forma más sencilla de solucionar esto en mis pruebas era simplemente establecer el valor del atributo de error en mi simulacro como Exception . Por ejemplo

El método que quiero probar tiene este patrón básico:

 def some_meth(self): try: struct.unpack(fmt, data) except struct.error: return False return True 

La prueba tiene este patrón básico.

 @mock.patch('my_module.struct') def test_some_meth(self, struct_mock): '''Explain how some_func should work.''' struct_mock.error = Exception self.my_object.some_meth() struct_mock.unpack.assert_called() struct_mock.unpack.side_effect = struct_mock.error self.assertFalse(self.my_object.some_meth() 

Esto es similar al enfoque adoptado por @BillB, pero es ciertamente más sencillo ya que no necesito agregar importaciones a mis pruebas y aún así obtener el mismo comportamiento. A mi parecer, esta es la conclusión lógica del hilo general del razonamiento en las respuestas aquí.