DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Python: pytest and Flask template context processor functions.
Be Hai Nguyen
Be Hai Nguyen

Posted on

Python: pytest and Flask template context processor functions.

We have some functional pytest tests, e.g. tests which retrieve HTML contents. The generation of these HTML contents uses some Flask template context_processor functions. These functions are available to the Flask application instance which is created via the Application Factory pattern. How do we make these same context_processor functions available in the pytest application instance which is also created via the same Application Factory pattern? We're discussing this question, and also pytest application fixture and test client fixure.

I had to write some pytest methods which make requests to routes. Some of these requests return HTML contents. The generation of those contents uses Flask template context_processor functions. That is, functions which are decorated with:

@app.context_processor
Enter fullscreen mode Exit fullscreen mode

My tests failed since I haven't made these functions available to the pytest's application fixture yet. I searched for solutions, but I could not find any... I tested out what I've thought might work, and it does.

To summarise, the application fixture function in the pytest module conftest.py must decorate the pytest application instance with the same Flask template context_processor functions. I.e.:

File D:\project_name\tests\conftest.py
Enter fullscreen mode Exit fullscreen mode
@pytest.fixture(scope='module')
def app():
    ...
    app = create_app()
    ... 

    app.app_context().push()
    """
    Making all custom template functions available 
    to the test application instance.
    """ 
    from project_name.utils import context_processor

    return app
Enter fullscreen mode Exit fullscreen mode

Basically, we create the test application instance using the Application Factory pattern function as per the real application instance.

app.app_context().push()
Enter fullscreen mode Exit fullscreen mode

The above line makes a valid context for the test application instance. Without a valid context, we'll get a working outside of the application context error message. It seems that with app.app_context().push() we have to call only once, then the context is available throughout, whereas with app.app_context():, the context is available only within the with:'s scope.

Then, the import call

from project_name.utils import context_processor
Enter fullscreen mode Exit fullscreen mode

decorates the test application instance with all Flask template context_processor functions implemented in module:

D:\project_name\src\project_name\utils\context_processor.py
Enter fullscreen mode Exit fullscreen mode

That is the gist of it... I'm demonstrating this with a proper project and tests in the following sections.

✿✿✿

Table of contents

Initial project code

We'll be using the existing extremely simple app_demo project, which has been created for other previous posts. Please get it using:

git clone -b v1.0.0 https://github.com/behai-nguyen/app-demo.git
Enter fullscreen mode Exit fullscreen mode

It has only a single route: http://localhost:5000/ -- which displays Hello, World!

To recap, the layout of the project is:

D:\app_demo\
|
|-- .env
|-- app.py
|-- setup.py
|
|-- src\
|   |
|   |-- app_demo\
|       |   
|       |-- __init__.py
|       |-- config.py
|
|-- venv\
Enter fullscreen mode Exit fullscreen mode

We'll build another /echo route using Flask Blueprint, and write tests for all two ( 2 ) routes.

Project layout when completed

The diagram below shows the project layout when completed. Please note β˜… indicates new files, and β˜† indicates files to be modified:

D:\app_demo\
|
|-- .env β˜†
|-- app.py β˜†
|-- setup.py β˜†
|-- pytest.ini β˜…
|
|-- src\
|   |
|   |-- app_demo\
|       |   
|       |-- __init__.py β˜†
|       |-- config.py
|       |-- urls.py β˜…
|       |
|       |-- controllers\ β˜…
|       |   |
|       |   |-- __init__.py
|       |   |-- echo.py
|       |   
|       |-- utils\ β˜…
|       |   |
|       |   |-- __init__.py 
|       |   |-- context_processor.py
|       |   |-- functions.py
|       |
|       |-- templates\ β˜…
|       |   |
|       |   |-- base_template.html
|       |   |-- echo\
|       |   |   |
|       |   |   |--echo.html
|       
|-- tests β˜…
|   |
|   |-- conftest.py 
|   |-- functional\
|       |
|       |-- test_routes.py
|
|-- venv\
Enter fullscreen mode Exit fullscreen mode

I've tested this project under Synology DS218, DSM 7.1-42661 Update 3, running Python 3.9 Beta; and Windows 10 Pro, version 10.0.19044 build 19044, running Python 3.10.1.

The finished codes for this post can be downloaded using:

git clone -b v1.0.4 https://github.com/behai-nguyen/app-demo.git
Enter fullscreen mode Exit fullscreen mode

Please note, the tag is v1.0.4. Please ignore all Docker related files.

Install required packages for pytest

We need packages pytest and coverage. Updated setup.py to include these two, then install the project in edit mode with:

(venv) D:\app_demo>venv\Scripts\pip.exe install -e .
(venv) behai@omphalos-nas-01:/volume1/web/app_demo$ sudo venv/bin/pip install -e .
Enter fullscreen mode Exit fullscreen mode

The echo.html template and the context_processor.py module

The echo.html template

This is echo.html in its entirety. It's pretty simple, just enough to demonstrate Flask template context_processor function print_echo( request ):

{% set echo = print_echo( request ) %}
Enter fullscreen mode Exit fullscreen mode

We store the value returned from print_echo( request ) to template variable echo. Then we just print out the content of this variable. If it is a POST request, then we print out the list of the key, value pairs that've been submitted. The β€œDate Time” line is to make the HTML content looks a bit dynamic.

To submit POST requests to http://localhost:5000/echo I'm using the The Postman App -- in the Body tab, select x-www-form-urlencoded, and then enter data to be submitted into the provided list. Click Send -- we should see HTML responses come back in the response section below.

The context_processor.py module

This is context_processor.py in its entirety. It has only a single simple function. I don't think it would require any explanation. The key issue, in my understanding:

...
from flask import current_app as app

@app.context_processor
def print_echo():
    def __print_echo( request ):
        ...
        return data

    return dict( print_echo=__print_echo )
Enter fullscreen mode Exit fullscreen mode

We must use the current_app from Flask, since we decorate the template function with:

@app.context_processor
def print_echo():
Enter fullscreen mode Exit fullscreen mode

current_app is defined as:

A proxy to the application handling the current request.

https://flask.palletsprojects.com/en/2.1.x/api/#flask.current_app

It should make sense, since the application instance could be an instance of a development web server, or an instance from a pytest as we're currently discussing.

We understand that this is only a demo method, so we make up the data for this purpose. For real applications, the data could come from sources such as a database, computed data, etc. And also we can have as many methods as we like.

The application entry point module app.py

As mentioned previously, context processor functions must be made available to the current running application instance. The updated application entry point module app.py:

...
with app.app_context():
    from app_demo.utils import context_processor
Enter fullscreen mode Exit fullscreen mode

loads up the context processor function discussed in The context_processor.py module for the current proper application instance just created. Please note:

...
with app.app_context():
Enter fullscreen mode Exit fullscreen mode

without the above call, it will result in RuntimeError: Working outside of application context. error.

The controller codes

The controller __init__.py module

controllers_init_.py defines a Flask Blueprint instance echo_blueprint.

The controller echo.py module

The module controllers\echo.py, has only a single one-line function which just renders and returns the echo.html template discussed in The echo.html template.

The urls.py module and the factory pattern __init__.py module

app_demo\urls.py defines a URL mapper list, and a list of available Flask Blueprint instances.

The /echo route supports both GET and POST request methods. And it is mapped to the echo_blueprint instance discussed in Controller init.py module, and the response method which serves the HTML content is the do_echo() method discussed in Controller echo.py module.

The Application Factory pattern module app_demo_init_.py has been updated to support the /echo route. The changes are extracted below:

...
from app_demo.utils.functions import template_root_path

def create_app():
    app = Flask( 'dsm-python-demo', template_folder=template_root_path() )

    ...

    app.url_map.strict_slashes = False

    ...

    register_blueprints( app )

    ...

def register_blueprints( app ):
    ...
Enter fullscreen mode Exit fullscreen mode

The application instance is now assigned template_folder. Turning off strict_slashes to make /echo and /echo/ the same route. And finally calls to the new function register_blueprints to register Flask Blueprint instance(s) and URL(s) discussed above.

The tests

This is the main part of this post... It takes awhile go get here πŸ˜‚.

pytest entry module conftest.py

The app() fixture

Let's look at the tests/conftest.py:

@pytest.fixture(scope='module')
def app():
    """
    Application fixure. 
    """
    app = create_app()

    app.app_context().push()
    """
    Making all custom template functions available 
    to the test application instance.
    """ 
    from app_demo.utils import context_processor

    return app
Enter fullscreen mode Exit fullscreen mode

The above method creates the test application instance using the same Application Factory pattern as per the application proper. It then creates a valid context for the test application instance via calling app.app_context().push(). Next, which is what we have been trying to get at -- it loads up the context processor function discussed in The context_processor.py module for the test application instance just created. This is exactly the same as for the application proper discussed in The application entry point module app.py.

Please note, for this post, none of the tests use this method directly, however this will be the test structure that I follow from now on. Anyhow, it will be used by the test_client( app ) fixture -- which we will look at next.

The test_client( app ) fixture
@pytest.fixture(scope='module')
def test_client( app ):
    """
    Creates a test client.
    app.test_client() is able to submit HTTP requests.

    The app argument is the app() fixure above. 
    """
    with app.test_client() as testing_client:
        yield testing_client  # Return to caller.
Enter fullscreen mode Exit fullscreen mode

The argument app which is the app() fixture who will get called automatically. For me, personally, I think of app.test_client() as a web browser, a mini-Postman, etc., which enables us to make HTTP requests.

Since the app() fixture already comes with a context via calling app.app_context().push() itself, we can call app.test_client() without result in working outside of the application context error message.

The test_routes.py module

There's only a single test module -- functional\test_routes.py:

...
@pytest.mark.hello_world
def test_hello_world( test_client ):
    ...

@pytest.mark.echo
def test_echo_get_1( test_client ):
    ...

@pytest.mark.echo
def test_echo_get_2( test_client ):
    ...

@pytest.mark.echo
def test_echo_post( test_client ):
    ...
Enter fullscreen mode Exit fullscreen mode

@pytest.mark.hello_world and @pytest.mark.echo are optional -- which enable us to run specific tests rather than all tests:

(venv) D:\app_demo>venv\Scripts\pytest.exe -m echo
(venv) D:\app_demo>venv\Scripts\pytest.exe -m hello_world
(venv) behai@omphalos-nas-01:/volume1/web/app_demo$ venv/bin/pytest -m echo
(venv) behai@omphalos-nas-01:/volume1/web/app_demo$ venv/bin/pytest -m hello_world
Enter fullscreen mode Exit fullscreen mode

hello_world and echo are defined in the pytest.ini config file.

The argument test_client to all test methods is The test_client( app ) fixture discussed previously. Test methods make use of its get() and post() methods to make requests, and then look into HTML responses for specific texts which we expected to be in the responses.

Flask latest version and .env file

I did un-install Flask to get the latest version installed. The latest version gives this warning:

'FLASK_ENV' is deprecated and will not be used in Flask 2.3. Use 'FLASK_DEBUG' instead.
Enter fullscreen mode Exit fullscreen mode

Environment file .env has been updated with FLASK_DEBUG=True to get rid of the warning.

Synology DS218 tests

As mentioned before, this project works under Linux:

034-01-synology-test.png

Codes download

To recap, the codes for this post can be downloaded using:

git clone -b v1.0.4 https://github.com/behai-nguyen/app-demo.git
Enter fullscreen mode Exit fullscreen mode

Please note, the tag is v1.0.4. Please ignore all Docker related files.

Concluding remarks

I have enjoyed working on this project. Particularly explaining the app() fixture and the test_client( app ) fixture in my own words. I have found these two a bit difficult to understand when I first looked at pytest.

Successfully applying Flask template context_processor functions to the test application instance is also satisfied.

Most of all, I hope this information can help somebody down the track. I hope you find this useful... and thank you for reading.

Top comments (0)

πŸ€” Did you know?

Β 
✍️ Writing your own article is easy (we even support markdown).