Не удается поймать издеваемое исключение, поскольку оно не наследует BaseException
Я работаю над проектом, который включает подключение к удаленному серверу, ожидание ответа, а затем выполнение действий на основе этого ответа. Мы поймаем несколько различных исключений и ведем себя по-разному в зависимости от того, какое исключение поймали. Например:
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
Чтобы проверить это, мы написали тест, подобный следующему
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
)
Если я запускаю функцию напрямую, все происходит так, как ожидалось. Я даже тестировал, добавляя raise requests.exceptions.ConnectionError
к предложению try
функции. Но когда я запускаю свои модульные тесты, я получаю
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
Я попытался изменить исключение, которое я исправлял в BaseException
и получил более или менее идентичную ошибку.
Я уже читал qaru.site/info/801068/..., поэтому я думаю, что это должен быть плохой __del__
крючок где-то, но я не уверен, где его искать или что я могу сделать даже в среднее время. Я также относительно новичок в unittest.mock.patch()
поэтому очень возможно, что я тоже делаю что-то неправильно.
Это надстройка Fusion360, поэтому она использует пакетную версию Python 3.3 Fusion 360 - насколько я знаю, это ванильная версия (т.е. Они не сворачиваются), но я не уверен в этом.
Ответы
Ответ 1
Я мог бы воспроизвести ошибку с минимальным примером:
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"
Испытание без насмешек:
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())
Хорошо, все в порядке, оба теста проходят
Проблема исходит от насмешек. Как только класс MyError высмеивается, предложение expect
ничего не может поймать, и я получаю ту же ошибку, что и пример из вопроса:
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())
Немедленно дает:
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 "<pyshell#78>", 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
Здесь я получаю первый TypeError
, которого у вас не было, потому что я поднимаю макет, когда вы принудительно генерировали истинное исключение с помощью 'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError
в config. Но проблема остается в том, что предложение except
пытается поймать макет.
TL/DR: когда вы издеваетесь над пакетом с полным requests
, except requests.exceptions.ConnectionError
excepts.exceptions.ConnectionError пытается поймать макет. Поскольку макет не является BaseException
, он вызывает ошибку.
Единственное решение, которое я могу себе представить, заключается не в том, чтобы высмеивать полные requests
а только за те части, которые не являются исключениями. Должен признаться, я не мог найти, как сказать, чтобы высмеивать все, кроме этого, но в вашем примере вам нужно только исправить requests.head
. Поэтому я думаю, что это должно работать:
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
)
То есть: исправлять только метод head
с исключением как побочный эффект.
Ответ 2
Я просто столкнулся с тем же вопросом, пытаясь издеваться над sqlite3
(и нашел этот пост при поиске решений).
Что сказал Серж, верно:
TL/DR: когда вы издеваетесь над пакетом с полным запросом, исключение excepts.exceptions.ConnectionError пытается поймать макет. Поскольку макет не является базовым исключением, он вызывает ошибку.
Единственное решение, которое я могу себе представить, заключается не в том, чтобы высмеивать полные запросы, а только за те части, которые не являются исключениями. Должен признаться, я не мог найти, как сказать, чтобы насмехаться над всем, кроме этого
Мое решение состояло в том, чтобы издеваться над всем модулем, а затем установить атрибут mock для исключения равным исключению в реальном классе, эффективно "высмеивая" исключение. Например, в моем случае:
@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)
Для requests
вы можете отдельно назначать исключения следующим образом:
mock_requests.exceptions.ConnectionError = requests.exceptions.ConnectionError
или сделать это для всех requests
например:
mock_requests.exceptions = requests.exceptions
Я не знаю, является ли это "правильным" способом сделать это, но пока это, похоже, работает для меня без каких-либо проблем.
Ответ 3
Для тех из нас, кто должен высмеивать исключение и не может этого сделать, просто перетаскивая head
, вот простое решение, которое заменяет целевое исключение пустым:
Скажем, у нас есть общий блок для тестирования, за исключением того, что мы должны высмеивать:
# app/foo_file.py
def test_me():
try:
foo()
return "No foo error happened"
except CustomError: # <-- Mock me!
return "The foo error was caught"
Мы хотим высмеять CustomError
но поскольку это исключение, мы сталкиваемся с проблемами, если попытаемся исправить его, как и все остальное. Обычно вызов patch
заменяет цель с помощью MagicMock
но это не сработает. Mocks отличные, но они не ведут себя, как исключения. Вместо того, чтобы исправлять макет, давайте вместо этого дадим ему исключение-заглушку. Мы сделаем это в нашем тестовом файле.
# 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!
Так что с lambda
? Параметр new_callable
вызывает все, что мы ему даем, и заменяет цель возвратом этого вызова. Если мы пройдем прямо с нашего класса StubException
, он вызовет конструктор класса и исправит наш целевой объект экземпляром исключения, а не классом, который мы не хотим. Обернув его lambda
, он возвращает наш класс, как мы предполагаем.
Как только наше исправление завершено, объект stub_exception
(который буквально является нашим классом StubException
) может быть поднят и пойман, как если бы это был CustomError
. Ухоженная!
Ответ 4
Я столкнулся с подобной проблемой, пытаясь издеваться над пакетом sh. Хотя sh очень полезен, тот факт, что все методы и исключения определены динамически, затрудняет их издевку. Поэтому, следуя рекомендации документации:
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):
...
Ответ 5
Я просто столкнулся с той же проблемой, когда насмехалась над struct
.
Я получаю сообщение об ошибке:
TypeError: catching классы, которые не наследуются от BaseException, не допускаются
При попытке поймать struct.error
собранный из struct.unpack
.
Я обнаружил, что самый простой способ обойти это в моих тестах состоял в том, чтобы просто установить значение атрибута ошибки в моем макете как Exception
. Например
Метод, который я хочу протестировать, имеет этот базовый шаблон:
def some_meth(self):
try:
struct.unpack(fmt, data)
except struct.error:
return False
return True
Тест имеет этот базовый шаблон.
@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()
Это похоже на подход, применяемый @BillB, но это, безусловно, проще, поскольку мне не нужно добавлять импорт в мои тесты и по-прежнему получать одинаковое поведение. Мне кажется, что это логический вывод общей темы рассуждений в ответах здесь.