问题
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