Имитация исключения, вызванного методом класса с побочным эффектом, дает «не поднималось»

Примечание. Этот вопрос основан на предыдущем вопросе, который я задавал, но изменен в соответствии с этот ответ.

Используя side_effect, я пытаюсь поднять исключение 'URLError' при вызове макета, но я получаю ошибку DID NOT RAISE, которую не понимаю.

У меня есть класс Query с методом класса make_request_and_get_response, который может вызвать несколько исключений. Я не перехватываю исключение 'URLError' в методе get_response_from_external_api в main.py, поэтому я должен ожидать возбуждения и, впоследствии, имитирования его исключения.

запрос.py

from urllib.request import urlopen
import contextlib
import urllib

class Query:
    def __init__(self, a, b):
        self.a = a
        self.b = b

        self.query = self.make_query()


    def make_query(self):
        # create query request using self.a and self.b
        return query


    def make_request_and_get_response(self):  # <--- the 'dangerous' method that can raise exceptions
        with contextlib.closing(urlopen(self.query)) as response:
            return response.read().decode('utf-8')

main.py

from foo.query import *

def get_response_from_external_api(query):
    try:
        response = query.make_request_and_get_response()
    except urllib.error.HTTPError as e:
        print('Got a HTTPError: ', e)
    except Exception:
        print('Got a generic Exception!')
        # handle this exception


if __name__ == "__main__":    
    query = Query('input A', 'input B')
    result = get_response_from_external_api(query)
    return result

Используя pytest, я пытаюсь издеваться над этим «опасным» методом (make_request_and_get_response) с побочным эффектом для конкретного исключения. Затем я приступаю к созданию фиктивного объекта Query для использования при вызове make_request_and_get_response с ожиданием, что этот последний вызов вызовет исключение «URLError».

test_main.py

import pytest
from unittest.mock import patch
from foo.query import Query
from foo.main import get_response_from_external_api


class TestExternalApiCall:
    @patch('foo.query.Query')
    def test_url_error(self, mockedQuery):
        with patch('foo.query.Query.make_request_and_get_response', side_effect=Exception('URLError')):
            with pytest.raises(Exception) as excinfo:
                q= mockedQuery()
                foo.main.get_response_from_external_api(q)
            assert excinfo.value = 'URLError'
            # assert excinfo.value.message == 'URLError' # this gives object has no attribute 'message'

Приведенный выше тест выдает следующую ошибку:

>       foo.main.get_response_from_external_api(q)
E       Failed: DID NOT RAISE <class 'Exception'> id='72517784'>") == 'URLError'

Та же ошибка возникает, даже если я поймаю исключение 'URLError', а затем повторно вызову его в get_response_from_external_api.

Может ли кто-нибудь помочь мне понять, чего мне не хватает, чтобы иметь возможность вызвать исключение в pytest?


Обновить в соответствии с ответом @SimeonVisser:

Если я изменю main.py, чтобы удалить случай except Excpetion:

def get_response_from_external_api(query):
    try:
        response = query.make_request_and_get_response()
    except urllib.error.URLError as e:
        print('Got a URLError: ', e)
    except urllib.error.HTTPError as e:
        print('Got a HTTPError: ', e)

затем тест в test_main.py:

    def test_url_error2(self):
        mock_query = Mock()
        mock_query.make_request_and_get_response.side_effect = Exception('URLError')
        with pytest.raises(Exception) as excinfo:
            get_response_from_external_api(mock_query)
        assert str(excinfo.value) == 'URLError'

Тест проходит нормально.


person timmy78h    schedule 14.11.2019    source источник


Ответы (1)


Проблема в том, что get_response_from_external_api() уже ловит Exception и не поднимает его куда-либо за пределы этой функции. Поэтому, когда вы издеваетесь над запросом и заставляете make_request_and_get_response вызывать исключение, pytest не увидит его, потому что get_response_from_external_api() уже его перехватывает.

Если вы измените его на следующее, у pytest есть шанс его увидеть:

    except Exception:
        print('Got a generic Exception!')
        # handle this exception
        raise

Тестовый пример также не работает должным образом. Его можно упростить до следующего (немного проще создать фиктивный запрос напрямую и передать его):

import pytest
from unittest import mock
from foo.main import get_response_from_external_api


class TestExternalApiCall:
    def test_url_error(self):
        mock_query = mock.Mock()
        mock_query.make_request_and_get_response.side_effect = Exception('URLError')
        with pytest.raises(Exception) as excinfo:
            get_response_from_external_api(mock_query)
        assert str(excinfo.value) == 'URLError'

и затем тестовый пример проходит (в дополнение к вышеуказанному изменению с raise).


Ответ на вопрос 1:

Разница в том, что вы имитируете Query с помощью декоратора, но затем вы издеваетесь над классом foo.query.Query внутри тестового примера, чтобы вызвать это исключение. Но ни в коем случае mockedQuery на самом деле не меняется, чтобы делать что-то по-другому для этого метода. Итак, q — это обычный экземпляр mock.Mock() без каких-либо особенностей.

Вы можете изменить его на следующее (аналогично приведенному выше подходу):

import pytest
from unittest.mock import patch
from foo.query import Query
from foo.main import get_response_from_external_api


class TestExternalApiCall:
    @patch('foo.query.Query')
    def test_url_error(self, mockedQuery):
        with patch.object(mockedQuery, 'make_request_and_get_response', side_effect=Exception('URLError')):
            with pytest.raises(Exception) as excinfo:
                get_response_from_external_api(mockedQuery)
            assert str(excinfo.value) == 'URLError'

Ваш оператор with patch('foo.query.Query........ будет работать, если в любом месте кода он затем создаст экземпляр Query из этого места.

Ответ на вопрос 2:

Хм, я не могу это воспроизвести - тестовый пример, который у вас есть локально, такой же, как в вопросе? Мне пришлось изменить его, так как foo.main.get_response_from_external_api(q) не существует (foo не импортируется), поэтому я изменил его на вызов get_response_from_external_api(q) и продолжаю получать:

>               get_response_from_external_api(q)
E               Failed: DID NOT RAISE <class 'Exception'>
person Simeon Visser    schedule 14.11.2019
comment
Забыл про последнюю универсальную Exception. Спасибо за объяснение и пример кода! Два дополнительных вопроса: 1) не могли бы вы указать, где была моя ошибка с подходом с использованием декоратора патча и менеджером контекста патча по сравнению с вашим подходом с имитацией напрямую? 2) Мой тест пройден, даже если я * не подниму "URLError" повторно; просто удалив корпус except Exception, он работает. Это несколько отличается от того, что вы говорите, нет? - person timmy78h; 14.11.2019
comment
Большое спасибо за ваш отредактированный ответ, в котором обсуждаются мои последующие вопросы. Теперь я обновил свой вопрос, чтобы ответить на ваш ответ на вопрос 2. - person timmy78h; 14.11.2019
comment
@ timmy78h да, если вы используете тестовый пример, как в моем ответе, а затем удаляете except Exception, он действительно пройдет, потому что make_request_and_get_response вызывается в имитированном объекте запроса, а затем исключение действительно возникает за пределами функции (и затем pytest замечает, и тест проходит). Так что никаких сюрпризов; Я не вижу такого поведения с тестовым примером в вопросе. - person Simeon Visser; 15.11.2019
comment
Возможно, вы также захотите ответить на этот вопрос: stackoverflow.com/q/58898675/12367781 - person timmy78h; 17.11.2019