Не удается поймать издеваемое исключение, поскольку оно не наследует 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, но это, безусловно, проще, поскольку мне не нужно добавлять импорт в мои тесты и по-прежнему получать одинаковое поведение. Мне кажется, что это логический вывод общей темы рассуждений в ответах здесь.