Search code examples
pythonflaskpytestwerkzeug

How can I test the error message returned by a flask app using render_template()


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.


Solution

  • 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
            }