Не удается поймать имитированное исключение, поскольку оно не наследует 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, и я получил более или менее идентичную ошибку.

Я уже прочитал https://stackoverflow.com/a/18163759/3076272, поэтому я думаю, что это должно быть плохой __del__ где-то зацепился, но я не знаю, где его искать и что я могу сделать в это время. Я также относительно новичок в unittest.mock.patch(), так что вполне возможно, что я тоже делаю что-то не так.

Это надстройка Fusion360, поэтому она использует упакованную версию Fusion 360 Python 3.3 — насколько я знаю, это ванильная версия (т. Е. Они не выпускают свои собственные), но я не уверен в этом.


person Dan Oberlam    schedule 29.07.2015    source источник
comment
FWIW Мне удалось обойти это, используя stackoverflow.com/a/28507806/3076272, но я бы предпочел найти почему это происходило.   -  person Dan Oberlam    schedule 30.07.2015
comment
Является ли requests вашим собственным модулем или это модуль requests с сайта python-requests.org?   -  person Serge Ballesta    schedule 07.08.2015
comment
Один из python-requests.org   -  person Dan Oberlam    schedule 07.08.2015


Ответы (6)


Я мог бы воспроизвести ошибку с минимальным примером:

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 в конфигурации. Но проблема остается в том, что предложение except пытается поймать макет.

TL/DR: когда вы имитируете полный пакет requests, предложение except requests.exceptions.ConnectionError пытается поймать мокап. Поскольку макет на самом деле не является BaseException, он вызывает ошибку.

Единственное решение, которое я могу себе представить, это не издеваться над полным requests, а только над теми частями, которые не являются исключениями. Должен признаться, я не смог найти, как сказать mock макетировать все, кроме этого, но в вашем примере вам нужно только исправить 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 с исключением в качестве побочного эффекта.

person Serge Ballesta    schedule 07.08.2015
comment
Вы также можете установить правильное исключение для своего макета: from requests.exceptions import COnnectionError; mocked_requests.exceptions.ConnectionError = ConnectionError - person Natim; 02.01.2019

Я только что столкнулся с той же проблемой, пытаясь издеваться над sqlite3 (и нашел этот пост, ища решения).

То, что сказал Serge, верно:

TL/DR: когда вы имитируете полный пакет запросов, предложение exclude request.exceptions.ConnectionError пытается поймать макет. Поскольку макет на самом деле не является BaseException, он вызывает ошибку.

Единственное решение, которое я могу себе представить, - это не имитировать полные запросы, а только те части, которые не являются исключениями. Должен признаться, я не мог найти, как сказать издеваться над издеваться над всем, кроме этого

Мое решение состояло в том, чтобы издеваться над всем модулем, а затем установить атрибут 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

Я не знаю, правильный ли это способ сделать это, но пока он работает для меня без каких-либо проблем.

person Bill B    schedule 25.03.2016

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

# 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. Аккуратный!

person user2859458    schedule 03.09.2016
comment
Из любопытства, зачем вам вообще нужно издеваться над исключением? - person Dan Oberlam; 03.09.2016
comment
@Dannnno: Хороший вопрос. Мы будем имитировать исключение по той же причине, по которой мы имитируем что угодно — чтобы предотвратить запуск его логики в нашем модульном тесте. Обычно исключения просто наследуют базовый класс исключений и больше ничего не делают. В этом случае издеваться над ними бессмысленно, потому что исключение, которое мы извлекаем, и исключение, которым мы его заменяем, логически идентичны, но время от времени кто-то создает исключение, в котором есть некоторая логика, и в этом редком случае приятно иметь возможность издеваться над этим. - person user2859458; 06.09.2016
comment
Мне просто очень трудно представить ситуацию, когда исключение должно выполнять какую-либо логику, над которой стоит посмеяться. Если возможно, не могли бы вы рассказать о том, что происходит? На самом деле я работаю над патчем для этой проблемы, и если бы я мог указать обосновывающую причину, это очень помогло бы. - person Dan Oberlam; 06.09.2016
comment
В конце концов, ваше воображение правильное. На самом деле нет веской причины, по которой исключение, основанное на ошибке, должно иметь логику, которую стоит заглушить - это должно быть обработано в блоке try imo. Но иногда вам приходится работать с кодом других людей, которые не понимают принципа разделения ответственности. Если исключение не стоит имитировать/заглушать, то я бы импортировал исключение в свой тестовый файл, поднял его с помощью макета side_effect и либо позволил устройству перехватить исключение, либо утвердил, что оно не было перехвачено с помощью self.assertRaises. - person user2859458; 06.09.2016
comment
Тем не менее, я думаю, стоит упомянуть, что основная функция raise заключается в работе с классом Exception для выхода из стека вызовов, что не обязательно означает, что произошла ошибка. Некоторые библиотеки (обычно веб-фреймворки) используют это поведение для управления потоком программы на более высоком уровне, чем просто if и while. В таких случаях в модульном тесте часто стоит избегать логики. - person user2859458; 06.09.2016

Я столкнулся с похожей проблемой при попытке имитировать пакет 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):
        ...
person Wtower    schedule 04.02.2017

Я только что столкнулся с той же проблемой, когда издевался над struct.

Я получаю сообщение об ошибке:

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

person Grr    schedule 12.12.2017

Используйте patch.object, чтобы частично смоделировать класс.

Мой вариант использования:

import unittest
from unittest import mock
import requests

def test_my_function(self):
    response = mock.MagicMock()
    response.raise_for_status.side_effect = requests.HTTPError

    with mock.patch.object(requests, 'get', return_value=response):
        my_function()
person idontevenseethecode    schedule 02.06.2020