Mock exception raised from class method with side effect gives 'did not raise'

时光怂恿深爱的人放手 提交于 2020-01-07 07:59:09

问题


Note: This question is based on a previous question I asked but modified to be different according to this answer.

Using side_effect, I am trying to raise a 'URLError' exception when a mock is called but I get a DID NOT RAISE error that I do not understand.

I have a Query class with a class method make_request_and_get_response which can raise several exceptions. I am not catching the 'URLError' exception within the get_response_from_external_api method in main.py, so I should be able to expect to be raised and, subsequently, its exception mocked.

query.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

Using pytest, I am trying to mock that 'dangerous' method (make_request_and_get_response) with a side effect for a specific exception. Then, I proceed with creating a mocked Query object to use when calling the make_request_and_get_response with the expectation that this last call give a 'URLError' exception.

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'

The test above gives the following error:

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

The same error occurs even if I catch the 'URLError' exception and then re-raise it in get_response_from_external_api.

Can someone help me understand what I am missing in order to be able to raise the exception in a pytest?


Update according to @SimeonVisser's response:

If I modify main.py to remove the except Excpetion case:

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)

then the test in 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'

The test passes OK.


回答1:


The issue is that get_response_from_external_api() already catches an Exception and doesn't raise it to anywhere outside that function. So when you mock the query and make make_request_and_get_response raise an exception, it won't be seen by pytest because get_response_from_external_api() already catches it.

If you change it to be the following then pytest has a chance of seeing it:

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

The test case also doesn't appear to work as intended. It can be simplified to the following (it's a bit easier to create a mock query directly and pass that around):

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'

and then the test case passes (in addition to also making the above change with raise).


Response to question 1:

The difference is that you're mocking Query using the decorator but then you're mocking the class foo.query.Query inside the test case to raise that exception. But at no point does mockedQuery actually change to do anything differently for that method. So q is a regular instance of mock.Mock() without anything special.

You could change it to the following (similar to the above approach):

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'

Your with patch('foo.query.Query........ statement would work if anywhere in the code it then instantiated a Query instance from that location.

Response to question 2:

Hmm, I can't reproduce that - is the test case that you have locally the same as in the question? I had to modify it as foo.main.get_response_from_external_api(q) doesn't exist (foo isn't being imported) so I changed it to call get_response_from_external_api(q) and I keep getting:

>               get_response_from_external_api(q)
E               Failed: DID NOT RAISE <class 'Exception'>


来源:https://stackoverflow.com/questions/58864101/mock-exception-raised-from-class-method-with-side-effect-gives-did-not-raise

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!