pytest will not raise exception - python

I can not get the exception to throw on this unit test.
def test_something(monkeypatch):
# Arrange
os.environ["ENTITY"] = "JURKAT" # should monkeypatch this, but ignore for now.
opgave_or_sag_id = "S7777777"
url = "https://testdriven.io"
auth = ("test", "test")
tx = 999999999
patch_called = False
def mock_get(*args, **kwargs):
nonlocal patch_called
patch_called = True
return MockResponseGet410()
monkeypatch.setattr(Session, "get", mock_get)
# Act
with pytest.raises(Exception) as exc_info:
dsu.fetch_entity_from_green(opgave_or_sag_id, url, auth, tx)
# Assert
assert patch_called
assert exc_info.value.args[0] == "Expected status code 200 (or 410 with invalid opgaver), but got 410 for entity JURKAT. Error: Invalid opgave - It's gone - https://testdriven.io"
class MockResponseGet410:
def __init__(self):
self.status_code = 410
self.text = "the opgave has status: INVALID, it's gone now."
self.reason = "It's gone"
self.url = "https://testdriven.io"
self.headers = {
"Content-Type": "application/json",
"Content-Length": "123",
"Content-Location": "tx=123",
}
# From dsu
def fetch_entity_from_green(opgave_or_sag_id, url, auth, tx):
"""Retrieve missing entity from green's api.
Parameters
----------
opgave_or_sag_id : string, required
tx : int, required
A tx number.
url : str, required
auth : AWSAuthObject, required
Returns
-------
entity : dict
A dict representing the entity from green.
status_code : int
"""
try:
ENTITY = os.environ["ENTITY"]
url_with_id = url + str(opgave_or_sag_id)
s = fetch_request_session_with_retries()
r = s.get(url_with_id, auth=auth)
handle_non_200_response_for_invalid_opgaver(r, opgave_or_sag_id, ENTITY)
# I deleted the rest, not relevant in this test as the above function should throw an exception.
except Exception as e:
print(f"An exception occured on request with id {opgave_or_sag_id}: {e}")
The exception should be thrown in handle_non_200_response_for_invalid_opgaver because mock_get returns a 410 status code and ENTITY is set to JURKAT:
def handle_non_200_response_for_invalid_opgaver(request, opgave_or_sag_id, ENTITY):
"""
Handles a non-200 response from the API but allows 410 responses on invalid opgaver.
"""
# 410 because Team Green returns this for invalid opgaver, which becomes a valid response.
if request.status_code != 200 and (
request.status_code == 410 and ENTITY != "OPGAVER"
):
print(f"Status code for request on {opgave_or_sag_id}: {request.status_code}")
raise Exception( # TODO be more explicit with exception.
f"Expected status code 200 (or 410 with invalid opgaver), but got {request.status_code} for entity {ENTITY}. Error: {request.text} - {request.reason} - {request.url}"
)
I can get an exception to throw using pytest.raises(Exception) in a different test (see below), and the test passes, so I'm on the right track:
def test_handle_non_200_response():
# Arrange
r = MockResponse()
# Act
with pytest.raises(Exception) as exc_info:
handle_non_200_response(r)
# Assert
assert (
exc_info.value.args[0]
== "Expected status code 200, but got 504. Error: Gateway Timeout - Exceeded 30 seconds - https://testdriven.io"
)
class MockResponse:
def __init__(self):
self.status_code = 504
self.text = "Gateway Timeout"
self.reason = "Exceeded 30 seconds"
self.url = "https://testdriven.io"
def json(self):
return {"id": 1}
def handle_non_200_response(request):
"""
Handles a non-200 response from the API.
"""
if request.status_code != 200:
print(f"Status code for request on {id}: {request.status_code}")
raise Exception(
f"Expected status code 200, but got {request.status_code}. Error: {request.text} - {request.reason} - {request.url}"
)
Can you see where I have gone astray?

Related

Airflow SimpleHttpOperator

Hi I am experiencing weird behavior from SimpleHttpOperator.
I have extended this operator like this:
class EPOHttpOperator(SimpleHttpOperator):
"""
Operator for retrieving data from EPO API, performs token validity check,
gets a new one, if old one close to not valid.
"""
#apply_defaults
def __init__(self, entity_code, *args, **kwargs):
super().__init__(*args, **kwargs)
self.entity_code = entity_code
self.endpoint = self.endpoint + self.entity_code
def execute(self, context):
try:
token_data = json.loads(Variable.get(key="access_token_data", deserialize_json=False))
if (datetime.now() - datetime.strptime(token_data["created_at"],
'%Y-%m-%d %H:%M:%S.%f')).seconds >= 19 * 60:
Variable.set(value=json.dumps(get_EPO_access_token(), default=str), key="access_token_data")
self.headers = {
"Authorization": f"Bearer {token_data['token']}",
"Accept": "application/json"
}
super(EPOHttpOperator, self).execute(context)
except HTTPError as http_err:
logging.error(f'HTTP error occurred during getting EPO data: {http_err}')
raise http_err
except Exception as e:
logging.error(e)
raise e
And I have written a simple unit test:
def test_get_EPO_data(requests_mock):
requests_mock.get('http://ops.epo.org/rest-services/published-data/publication/epodoc/EP1522668',
text='{"text": "test"}')
requests_mock.post('https://ops.epo.org/3.2/auth/accesstoken',
text='{"access_token":"test", "status": "we just testing"}')
dag = DAG(dag_id='test_data', start_date=datetime.now())
task = EPOHttpOperator(
xcom_push=True,
do_xcom_push=True,
http_conn_id='http_EPO',
endpoint='published-data/publication/epodoc/',
entity_code='EP1522668',
method='GET',
task_id='get_data_task',
dag=dag,
)
ti = TaskInstance(task=task, execution_date=datetime.now(), )
task.execute(ti.get_template_context())
assert ti.xcom_pull(task_ids='get_data_task') == {"text": "test"}
Test doesn't pass though, the XCOM value from HttpHook is never pushed as an XCOM, I have checked that code responsible for the push logic in the hook class gets called:
....
if self.response_check:
if not self.response_check(response):
raise AirflowException("Response check returned False.")
if self.xcom_push_flag:
return response.text
What did I do wrong? Is this a bug?
So I actually managed to make it work by setting an xcom value to the result of super(EPOHttpOperator, self).execute(context).
def execute(self, context):
try:
.
.
.
self.headers = {
"Authorization": f"Bearer {token_data['token']}",
"Accept": "application/json"
}
super(EPOHttpOperator, self).execute(context) -> Variable.set(value=super(EPOHttpOperator, self).execute(context),key='foo')
Documentation is kind of misleading on this one; or am I doing something wrong after all?

Django unittest with not raising exception as expected with assertRaises()

I'm writing a unittest class to ensure a method tests for a success, and then tests for an Exception. I'm passing a response that should trigger the exception, but in the testing method it does not get raised. Of note, I can manually make the exception raise in the actual method.
Test class:
class TestAPI(TestCase):
def test_send_method(self):
with mock.patch('requests.post') as mock_request:
mock_response = mock.Mock()
mock_response.json.return_value = {
"success": "true"
}
mock_request.return_value = mock_response
send_method() // THIS WORKS NICELY
# Test that errors from the API are handled correctly.
with self.assertRaises(SendException):
mock_response.status_code = 500
mock_response.json.return_value = {
'errors': 'An error has occurred.',
}
send_method() // THIS RAISES NO EXCEPTION
As I said, It's odd because I can manually trigger the 500 status code in the actual method and it raises fine. I can even change the initial mock response success to err and it will raise in the actual method. Why would it not raise in the unittest?
Method being tested:
class SendException(Exception):
pass
def send_method():
session_headers = {
"Content-Type": "application/json",
}
session_body = {
"send": "yes"
}
session_url = u'{}'.format(URL)
session_response = requests.post(session_url, json=session_body, headers=session_headers)
try:
if(session_response.json().get('errors') is not None):
raise SendException(
'Returned error with status {}: {}'.format(
session_response.status_code,
session_response.json().get('errors')
)
)
except ValueError as err:
raise SendException(
'Responded with non-json and status code {}. Error: {} - Response Text: {}'.format(
session_response.status_code,
err,
session_response.text
)
)

Instancemethod object is not iterable (AirPi) (Python)

I got ERROR:Exception during output: 'instancemethod' object is not iterable when debugging this AirPi code from https://github.com/haydnw/AirPi/blob/master/outputs/ubidots.py
This suppose to upload my sensor data to the Ubidots server.
*I'd put my correct token and variable ID inside the configuration file for this AirPi.
requiredSpecificParams = ["token"]
optionalSpecificParams = ["showcost",
"ID-BMP085-temp",
"ID-BMP085-pres",
"ID-DHT22-hum",
"ID-DHT22-temp",
"ID-LDR",
"ID-TGS2600",
"ID-MiCS-2710",
"ID-MiCS-5525",
"ID-Microphone",
"ID-Raingauge"
]
def __init__(self, config):
super(Ubidots, self).__init__(config)
self.token = self.params["token"]
if "showcost" in self.params:
self.showcost = self.params["showcost"]
else:
self.showcost = False
self.ubivariables = {}
for key, value in self.params.iteritems():
if key[:3] == "ID-":
if value:
self.ubivariables[key[3:]] = value
def output_data(self, datapoints, dummy):
"""Output data.
Output data in the format stipulated by the plugin. Calibration
is carried out first if required.
Because this particular plugin (ubidots) does not show time, the
third argument (normally called 'sampletime') is called 'dummy'
to facilitate compliance with pylint.
Args:
self: self.
datapoints: A dict containing the data to be output.
dummy: datetime representing the time the sample was taken.
Returns:
boolean True if data successfully output to Ubidots; False if
not
"""
if self.params["calibration"]:
datapoints = self.cal.calibrate(datapoints)
payload = []
for point in datapoints:
for ubivariablename, ubivariableid in self.ubivariables.iteritems():
if point["sensor"] == ubivariablename:
if point["value"] is not None:
thisvalue = {}
thisvalue["variable"] = ubivariableid
thisvalue["value"] = point["value"]
payload.append(thisvalue)
break
headers = {'Accept': 'application/json; indent=4', 'Content-Type': 'application/json', 'X-Auth-Token': self.token}
url = "http://things.ubidots.com/api/v1.6/collections/values"
req = None
cost = 0
try:
req = requests.post(url, data=json.dumps(payload), headers=headers)
except Exception, e:
print("ERROR: Failed to contact the Ubidots service.")
print("ERROR: " + str(e))
return False
for response in req.json:
if response["status_code"] is not 201:
print("ERROR: Ubidots responded with an error for one of the values.")
return False
else:
cost += 1
if self.showcost:
print("Ubidots upload cost " + str(cost) + " dots.")
return True
for response in req.json:
According to the documentation, json is a method and must be called, so this should be:
for response in req.json():
In the future it is helpful to include just as much of your code as is necessary to reproduce the problem, and to include the complete error message with traceback.

Preserve response context testing flask app with pytest

I'm testing a flask application with py.test with the following code:
response = flask_app_test_client.post('/users', data=json.dumps(user))
assert response.status_code == 201
assert response.content_type == 'application/json'
assert isinstance(response.json, dict)
assert set(response.json.keys()) >= {'id', 'status', 'created_at', 'updated_at'}
assert response.json['name'] == user['name']
assert response.json['status'] == 'pending'
When some assertion fails I'm getting something like this:
response = test_client.post('/users', data=json.dumps(user))
> assert response.status_code == 201
E assert 400 == 201
E + where 400 = <JSONResponse streamed [400 BAD REQUEST]>.status_code
============== 1 failed, 3 passed in 0.10 seconds ===================
I do a lot of TDD so I expect my test fails frequently while developing. My problem is the assertion error message is kind of useless without the rest of the response data (body, headers, etc).
I only get in the output that the response.status_code is 400 but I don't get the error description that is in the response body: {"errors": ["username is already taken", "email is required"]}. Ideally I would like a full dump of the request and response (headers + body) when an assertion fails.
How I can print a summary of the response on each failed assertion?
Assert statement graamar
assert response.status_code == 201, "Anything you want"
You can be as verbose as you want. You can also use UnitTest's suite of helper methods -
without test case classes through this bit of abuse - https://github.com/nose-devs/nose2/blob/master/nose2/tools/such.py#L34
I'm came up with two different solutions.
Solution #1: try/catch
try:
assert response.status_code == 201
assert response.content_type == 'application/json'
assert isinstance(response.json, dict)
assert set(response.json.keys()) >= {'id', 'status', 'created_at', 'updated_at'}
assert response.json['name'] == user['name']
assert response.json['status'] == 'pending'
except AssertionError as e:
except AssertionError as e:
raise ResponseAssertionError(e, response)
class ResponseAssertionError(AssertionError):
def __init__(self, e, response):
response_dump = "\n + where full response was:\n" \
"HTTP/1.1 {}\n" \
"{}{}\n".format(response.status, response.headers, response.json)
self.args = (e.args[0] + response_dump,)
Solution #2: no try/catch needed (if repr is too long sometimes is cut off...)
Extend and override Flask response object
import json
class JSONResponse(Response):
def __repr__(self):
headers = {}
while len(self.headers) > 0:
tuple_ = self.headers.popitem()
headers[tuple_[0]] = tuple_[1]
data = {
'status': self.status,
'headers': headers,
'body': self.json
}
return json.dumps(data)
and
#pytest.fixture(scope='session')
def test_client(flask_app):
flask_app.response_class = JSONResponse
return flask_app.test_client()
I know this is an older question, but Pytest has an option --pdb that will pop you into a PDB shell should your test fail. Very handy way to "just look around" rather than having to pass tons of stuff to an exception message.

How to test next part of code when the request object has been mocked?

I have some a Python function written this way:
def verify_secondary_hostnames(self, publishedName, hostnames):
cname = self.get_cname(publishedName)
edge_ip = requests.get('http://{}'.format(cname)).headers['X-Edge-IP']
for hostname in hostnames:
headers = {"host": hostname}
data = requests.head("http://{}".format(edge_ip), headers=headers)
data.raise_for_status()
The unit test that I have tried to write for it looks like this:
#patch("requests.get")
#patch("requests.head")
#patch("FrontEnd.FrontEnd.get_cname")
def test_verify_secondary_hostnames(self, mock_get_cname, mock_head, mock_get):
mock_get_cname.return_value = "hulahoop"
mock_get.return_value.headers = {"X-Edge-IP": "hulahoop"}
self.fe.verify_secondary_hostnames('publishedName', ['hostname'])
self.assertEqual(mock_head.call_args,
call('http://hulahoop', headers={'host': 'hostname'}))
In this test the last part raise_for_status which actually does this:
def raise_for_status(self):
"""Raises stored :class:`HTTPError`, if one occurred."""
http_error_msg = ''
if 400 <= self.status_code < 500:
http_error_msg = '%s Client Error: %s' % (self.status_code, self.reason)
elif 500 <= self.status_code < 600:
http_error_msg = '%s Server Error: %s' % (self.status_code, self.reason)
if http_error_msg:
raise HTTPError(http_error_msg, response=self)
I can't do it here since I have already mocked the requests.head. How can I write my test to cover for that as well ? What I mean by "cover" here is that since the object is a mock object, How can I make it raise_for_status ? Do I have to create a mock method to do that or emulate a raise in my test ?
Edit: This is the latest test I have with the problems outlined:
#patch("requests.get")
#patch("requests.head")
#patch("FrontEnd.FrontEnd.get_cname")
#patch("requests.models.Response.raise_for_status")
def test_verify_secondary_hostnames(self, mock_raise,
mock_get_cname, mock_head, mock_get):
mock_get_cname.return_value = "hulahoop"
mock_get.return_value.headers = {"X-Edge-IP": "hulahoop"}
mock_head.return_value.status_code = 200
self.fe.verify_secondary_hostnames('publishedName', ['hostname'])
self.assertEqual(mock_head.call_args,
call('http://hulahoop', headers={'host': 'hostname'}))
mock_head.raise_for_status.called <=== False here
mock_head.reset_mock()
mock_head.return_value.status_code = 400
self.fe.verify_secondary_hostnames('publishedName', ['hostname'])
mock_head.raise_for_status.called <=== Still false here.
You can try something like this in your unit test:
mock_data = mock.MagicMock()
mock_data.status_code = 200 # TODO (Low Kian Seong): Test raise_for_status
mock_head.return_value = mock_data
...
mock_data.raise_for_status.assert_called_once_with()

Categories