I'm writing some unittests for a basic flask app. This app takes some data in as a post request and then fires it off to another service, before displaying the response to user on a web-page.
item_blueprint.py
from flask import Blueprint, render_template, request, redirect, session
import requests
from urllib.parse import urljoin
import os
item_blueprint = Blueprint('item_blueprint', __name__)
base_item_url = os.getenv('ITEM_SERVICE_URL')
@item_blueprint.route('/create-item', methods=['GET', 'POST'])
def create_item():
error = None
if session.get('item'):
session.pop('item')
if request.method == 'POST':
item_name = request.form['item_name']
payload = {"authentication": session['authentication'],
"item_data": {"name": item_name},
"sender": "frontend_service",
}
url = urljoin(base_item_url, 'item/create-item')
r = requests.post(url, json=payload)
response = r.json()
if response['code'] == 200:
session['item'] = response['payload']
return redirect('/update-item', code=302)
else:
error = response['payload']
return render_template('/item/create_item.html', error=error)
This works as expected, with no issues. However, when I try to test it, I cannot seem to access the error message in 'render template'.
I'm using pytest, so have a flask app and client fixture over in conftest.py
conftest.py
import pytest
from myapp.web.app import app as flask_app
@pytest.fixture()
def app():
yield flask_app
@pytest.fixture()
def client(app):
return app.test_client()
To avoid integration headaches the test itself mocks the requests.post
function and uses the flask session context.
test_item.py
from unittest import mock
mock_post_request = 'my_app.web.blueprints.item.requests.post'
authentication = {'auth_token': 'auth_token',
'user_id': 'user.id'}
class TestCreateItem:
new_item = {'item_name': 'test_item'}
def test_create_item_valid(self, app, client):
"""
GIVEN: An item_name has been sent to the create-item endpoint
WHEN: The endpoint returns a 200 code
THEN: The user is redirected to the update-item page
"""
with mock.patch(mock_post_request) as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {'payload': {'item': 'test_item'},
'code': 200,
}
with client.session_transaction() as self.session:
self.session['authentication'] = authentication
self.response = client.post('/create-item', data=self.new_item, follow_redirects=True)
assert self.response.status_code == 200
mock_post.assert_called_once()
assert len(self.response.history) == 1 #just one redirect
assert self.response.request.path == "/update-item"
def test_create_item_invalid(self, app, client):
"""
GIVEN: An item_name has been sent to the create-item endpoint
WHEN: The endpoint returns a non-200 code
THEN: The user remains on the create-item page
AND THEN: An error message is displayed
"""
with mock.patch(mock_post_request) as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {'payload': "error",
'code': 401,
}
with client.session_transaction() as self.session:
self.session['authentication'] = authentication
self.response = client.post('/create-item', data=self.new_item, follow_redirects=True)
assert self.response.status_code == 200
mock_post.assert_called_once()
assert len(self.response.history) == 0 # just one redirect
assert self.response.request.path == "/create-item"
assert self.response.request.args.get('error') == "error"
The first test case passes with no issues. The second fails because the final test (assert self.response.request.args.get('error')
) returns None.
I have looked through the docs and tried using self.response.request. +args, +form, +get_json, +data as a way of accessing the 'error' parameter, but all either return 'None' or a type error.
I had wondered if the issue was with the mock_post item, but essentially skipping it by hardcoding the error value in the render_template() method doesn't change the behaviour.
Any suggestions gratefully received.
I believe the condition WHEN: The endpoint returns a non-200 code
is not reflected correctly in the mock. So the else-branch is not tested actually.
def test_create_item_invalid(self, app, client):
"""
...
WHEN: The endpoint returns a non-200 code
...
"""
with mock.patch(mock_post_request) as mock_post:
# taken from previous tests probably:
# mock_post.return_value.status_code = 200
mock_post.return_value.status_code = 401 # should be
mock_post.return_value.json.return_value = {
'payload': "error",
'code': 401
}