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.
Python: pytest and Flask template context processor functions. |
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
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
@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
Basically, we create the test application instance using the Application Factory pattern function as per the real application instance.
app.app_context().push()
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
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
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
- Project layout when completed
- Install required packages for pytest
- The echo.html template and the context_processor.py module
- The application entry point module app.py
- The controller codes
- The urls.py module and the factory pattern __init__.py module
- The tests
- Flask latest version and .env file
- Synology DS218 tests
- Codes download
- Concluding remarks
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
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\
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\
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
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 .
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 ) %}
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 )
We must use the current_app from Flask, since we decorate the template function with:
@app.context_processor
def print_echo():
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
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():
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 ):
...
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
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.
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 ):
...
@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
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.
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:
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
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.