Course Table of Contents

It goes without saying on testdriven.io that app testing is extremely important. They say if it’s not tested, it’s already broken, and in my experience that’s absolutely true. At times in the past I’ve written what I thought was simple code, and deployed it to production without testing it, thinking surely it would work, only to be notified by a customer or a coworker that something was broken the next day… Learn from my mistakes and write tests for your code as you go.

Since our app is divided into Flask and Dash pages, we’ll start with testing the slightly simpler Flask pages in this chapter. Next chapter we’ll use Selenium WebDriver to perform front-end integration tests of the Dash/React single page application.

Flask Testing

For Python apps, there are two great choices for writing tests: the unittest module in the Python standard library, and pytest. I use both in different projects of mine, but for this course we’re going to use pytest.

Create a tests directory next to your app directory, and create four files in the directory:

  1. init.py - empty file signifying the tests directory is a Python package
  2. conftest.py - for pytest options config for Dash (ignore for now)
  3. test_flask.py
  4. test_dash.py

Ignore the conftest.py and test_dash.py files for now. Those are for the next chapter on Dash testing.

In test_flask.py add the following imports, and our first pytest “fixture”. Pytest fixtures are objects you can share among your various test functions. We’re going to be using the Flask app, and the Dash app, in our tests, so let’s create them once inside the flask_and_dash_tuple fixture function.

Note also we’re disabling Flask-WTF CSRF protection, just in our tests. This way we can easily send POST requests to the Flask test server to login with our demo user.

import pytest

from app import create_app, db
from app.database import check_db_tables
from app.models import User


@pytest.fixture(scope="module")
def flask_and_dash_tuple():
    """
    pytest fixture to share the Flask app
    among different tests, if desired
    """

    app, dashapp = create_app()

    # Disable CSRF protection for the unit tests
    app.config['WTF_CSRF_ENABLED'] = False

    with app.app_context():
        # This is where the testing happens
        yield app, dashapp

Our next fixture won’t be used so much as run to ensure the database tables are available, and to add a demo user so we can login. We’re using the check_db_tables() function to ensure the tables are setup.

Next we query the User model (the public.users database table) to see if the demo user has been added. If user returns as None (i.e. if not user), we’ll create the user, and add and commit the user to the database so we can login.

@pytest.fixture(scope="module")
def init_database(flask_and_dash_tuple):
    """Initialize the testing database"""

    flask_app, dashapp = flask_and_dash_tuple

    # Create the database and the database tables if necessary
    check_db_tables(flask_app, db)
 
    # Insert a demo user so we can login
    user = User.query.filter_by(email="demo@test.com").first()
    if not user:
        user = User(
            email="demo@test.com",
            password="password",
            first_name="Demo",
            last_name="User",
        )
        db.session.add(user)
    
        # Commit the changes to the database
        db.session.commit()
 
    # This is where the testing happens
    yield db

We’re finished with the pytest fixtures, so we can now create our first unit test–a simple one to start. The test starts with test_ so pytest can easily find it (all tests will start with test_ in this course), and the test function accepts our flask_and_dash_tuple pytest fixture as an argument.

Inside the test function, we first extract the flask_app, dashapp from the fixture, which is a tuple. We don’t need the dashapp for these Flask tests, so feel free to change that variable name to _ to signify it’s not going to be used.

Flask comes with a built-in test_client method for creating a test client, so we take advantage of that. We use the test_client to send a GET request to our base route, and assert that the status_code == 200 (successful request). We also check the response.data to ensure it contains our simple text and link to the Dash page, in binary format.

def test_main_flask_route(flask_and_dash_tuple):
    """
    Very simple Flask test to see if the main
    Flask route is accessible
    """

    flask_app, dashapp = flask_and_dash_tuple
    test_client = flask_app.test_client()

    # Send a GET request to the main Flask route
    response = test_client.get("/")

    # Test that it worked
    assert response.status_code == 200
    assert b'Click <a href="/dash/">here</a> to see the Dash single-page application (SPA)' in response.data

For our final Flask test, we are going to test the login and logout routes. Notice this test also uses the init_database pytest fixture to ensure the demo user has been added to the database.

This time we’re submitting a POST request with our login credentials, instead of a basic GET request as in the previous test.

Once the demo user is logged in, she is redirected to the “/dash/” page, where we assert the “react-entry-point” ID is in the response data. All Dash pages have that ID in the HTML.

After the user is logged in, we can test the logout route, which redirects back to our simple homepage.

def test_login_and_logout(flask_and_dash_tuple, init_database):
    """
    Given a Flask application,
    when the "/login" page is posted to (POST),
    check the response is valid
    """
    
    flask_app, dashapp = flask_and_dash_tuple
    test_client = flask_app.test_client()
    response = test_client.post(
        "/login/",
        data=dict(
            email="demo@test.com",
            password="password"
        ),
        follow_redirects=True
    )
    assert response.status_code == 200

    # The 'react-entry-point' ID is in the Dash app, where we get redirected after login
    assert b'id="react-entry-point"' in response.data
 
    """
    Given a Flask application,
    when the "/logout" page is requested (GET),
    check the response is valid
    """
    response = test_client.get("/logout/", follow_redirects=True)
    assert response.status_code == 200

    # We should be redirected back to the simple homepage
    assert b'Click <a href="/dash/">here</a> to see the Dash single-page application (SPA)' in response.data

To run these tests, just run pytest in your shell. Here are some optional arguments I like to add as well:

/workspace/tests/  # tests' location
-v  # verbose Pytest logging
--lf  # run the last-failed test first
-x  # stop after first failed test
--headless  # this makes Dash integration testing faster

That’s it for Flask testing. In the next chapter we’ll cover the more advanced Dash app testing with Selenium WebDriver.

Course Table of Contents