Preserve response context testing flask app with pytest - python

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.

Related

How to retrieve specific data from request in pytest

Hi so what I am trying to do is when I run the test the user will be created (makes an id of an user) and at the end of the test I want to delete that user. The way I decided to go to find that user is to pass the username I gave to that user in this case 'test123'.
So first I need the auth and get_user_by_username to run before delete. But when I put assert on user.status_code I get 422 instead of 200. When I try it in the swagger it works fine and I get 200 response
Here is the code:
def test_delete_new_users():
auth = client.post('/token',
data={'username': 'test123', 'password': 'test123'}
)
access_token = auth.json().get('access_token')
user = client.post(
'/user/get_user_by_username',
json={"username": "test123", }
)
assert user.status_code == 200
response = client.delete(
'/user/delete/' + user.json().get('user_id'),
headers={'Authorization': 'bearer' + access_token}
)
assert response.status_code == 200
When I remove the assert part of the code and just run it I get this error:
FAILED tests_main.py::test_delete_new_users - TypeError: can only concatenate str (not "NoneType") to str
this is get_user_by_username API:
#router.post('/get_user_by_username', response_model=UserDisplay)
async def get_user_by_username(username: str, db: Session = Depends(database.get_db)):
user = db.query(models.User).filter(models.User.username == username).first()
if user is None:
raise HTTPException(status_code=404, detail='Not found')
return user
This is the request and response on swagger:

pytest will not raise exception

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?

Mocking requests.post [duplicate]

This question already has answers here:
How can I mock requests and the response?
(20 answers)
Closed 2 years ago.
This is my first time writing unit tests, apologies for the annoyances inevitably present, despite my best efforts. I am trying to mock requests.post but my test function is not having the desired effect, to induce a 404 status code so that I can test error handling.
mymodule.py
def scrape(data):
logger.debug(f'\nBeginning scrape function')
result = {}
exceptions = {}
for id, receipts in data.items():
logger.debug(f'Looking up Id # {id} and receipts: \n{receipts}')
dispositions = []
for receipt in receipts:
logger.debug(f'The length of receipts is:' + str(len(receipts)))
attempts = 1
while attempts < 6:
logger.debug(
f'This is attempt number {attempts} to search for {receipt}')
payload = {'receipt': 'receipt',
'button': 'CHECK+STATUS', }
try:
NOW = datetime.today().strftime('%c')
logger.debug(NOW)
logger.debug(f'Making post request for: {receipt}')
response = requests.post(URL, data=payload, headers=HEADERS, timeout=10)
except Exception as e:
logger.debug(f'There was an exception: {e}')
exceptions[id] = receipt + f': {e}'
time.sleep(3)
attempts += 1
else:
logger.debug(f'It worked {response.status_code}')
attempts = 6
disp = parse(response)
dispositions.append(f'{receipt}: {disp}')
result[id] = dispositions
logger.debug(f'Here is the result: {result}')
return result
test_mymodule.py
def test_scrape(self):
print(f'\ntest_scrape running')
# mock a 404 in scrape() here
with patch("mymodule.requests") as patched_post:
# mock a request response
patched_post.return_value.status_code = 404
print('404 mocked')
# verify the function returns nothing due to 404
result = scrape(test_data)
print(f'\n{result}')
mock_requests.post.assert_called_once()
self.assertEqual(result, {})
def test_scrape(self):
print(f'\ntest_scrape running')
# mock a 404 in scrape() here
with patch("mymodule.requests") as patched_post:
# mock a request response
patched_post.return_value.status_code = 404
print('404 mocked')
# verify the function returns nothing due to 404
result = scrape(test_data)
print(f'\n{result}')
mock_requests.post.assert_called_once()
self.assertEqual(result, {})

How to write testcase to verify 500 response for flask apis

I am trying to write unit test cases for flask APIs. I am unable to figure out how to write test cases for 500 internal server errors and custom exceptions in API response.
Here are my books api to get the book by id
#BOOKS_BP.route('/<book_id>', methods=['GET'])
def get_book(book_id):
try:
book = Book.query.filter(Book.id == book_id).scalar()
if not book:
raise DBRecordNotFound('No Record Found with ID - {}'.format(book_id))
book_schema = BookSchema()
return jsonify({'status': 'success', 'book': book_schema.dump(book)}), HTTPStatus.OK
except DBRecordNotFound as ex:
return jsonify({'status': 'error', 'message': str(ex)}), HTTPStatus.NOT_FOUND
except Exception as ex:
logger.error(ex, exc_info=True)
return jsonify({'status': 'error', 'message': str(ex)}), HTTPStatus.INTERNAL_SERVER_ERROR
Here is my get book by id test case.
import json
from tests.test_base import BaseTestCase
from tests.factories import BookFactory
from http import HTTPStatus
class BooksTestCase(BaseTestCase):
#classmethod
def setUpClass(cls):
super(BooksTestCase, cls).setUpClass()
book = BookFactory(name='Test Book')
book.save()
cls.test_book_id = book.id
def test_book_by_id(self):
# For 200 Status
response = self.client.get('/api/books/{}'.format(self.test_book_id))
data = json.loads(response.data)
assert response.status_code == HTTPStatus.OK
assert data['book']['name'] == 'Test Book'
# For 404 Status
response = self.client.get('/api/books/123456')
data = json.loads(response.data)
assert response.status_code == HTTPStatus.NOT_FOUND
assert data['message'] == 'No Record Found with ID - 123456'
# For 500 Status

Super-performatic comparison

I have a python code which recovers information from an HTTP API using the requests module. This code is run over and over again with an interval of few milliseconds between each call.
The HTTP API which I'm calling can send me 3 different responses, which can be:
text 'EMPTYFRAME' with HTTP status 200
text 'CAMERAUNAVAILABLE' with HTTP status 200
JPEG image with HTTP status 200
This is part of the code which handles this situation:
try:
r = requests.get(url,
auth=(username, pwd),
params={
'camera': camera_id,
'ds': int((datetime.now() - datetime.utcfromtimestamp(0)).total_seconds())
}
)
if r.text == 'CAMERAUNAVAILABLE':
raise CameraManager.CameraUnavailableException()
elif r.text == 'EMPTYFRAME':
raise CameraManager.EmptyFrameException()
else:
return r.content
except ConnectionError:
# handles the error - not important here
The critical part is the if/elif/else section, this comparison is taking way too long to complete and if I completely remove and simply replace it by return r.content, I have the performance I wish to, but checking for these other two responses other than the image is important for the application flow.
I also tried like:
if len(r.text) == len('CAMERAUNAVAILABLE'):
raise CameraManager.CameraUnavailableException()
elif len(r.text) == len('EMPTYFRAME'):
raise CameraManager.EmptyFrameException()
else:
return r.content
And:
if r.text[:17] == 'CAMERAUNAVAILABLE':
raise CameraManager.CameraUnavailableException()
elif r.text[:10] == 'EMPTYFRAME':
raise CameraManager.EmptyFrameException()
else:
return r.content
Which made it faster but still not as fast as I think this can get.
So is there a way to optimize this comparison?
EDIT
With the accepted answer, the final code is like this:
if r.headers['content-type'] == 'image/jpeg':
return r.content
elif len(r.text) == len('CAMERAUNAVAILABLE'):
raise CameraManager.CameraUnavailableException()
elif len(r.text) == len('EMPTYFRAME'):
raise CameraManager.EmptyFrameException()
Checking the response's Content-Type provided a much faster way to assure an image was received.
Comparing the whole r.text (which may contain the JPEG bytes) is probably slow.
You could compare the Content-Type header the server should set:
ct = r.headers['content-type']
if ct == "text/plain":
# check for CAMERAUNAVAILABLE or EMPTYFRAME
else:
# this is a JPEG

Categories