Testing Flask applications (code, database, views, flask config, and app context) with pytest

I love writing tests for my code but whenever starting in a new language or framework it's such a pain since getting the mocks and fixtures just right tends to be very language & framework specific. I searched and searched for a good pytest + flask configuration that would let me do unit and integration tests for the app and found some good pieces but nothing holistic.

Thorough testing of Flask apps

I wanted a pytest configuration that would give no side-effects between tests and provide a solid foundation for writing everything from unit to integration to database tests:

  • Mocked (monkey-patched) methods and classes for unit testing
  • Per-test Flask application context, letting you test things like oauth-filter-for-python-flask
  • Fast, in-memory SQLite database that is torn down between each test
  • REST client to test Flask APIs or views

pytest configuration

The code samples below are pretty generic but may require minor customization for your Flask application. I highly recommend you take a look at the flask-bones sample, which contains many best practices and this sample will work with it out of the box.

It assumes the use of the following modules available via pip: pytest, pytest-flask and pytest-mock

pytest's conftest.py:

import pytest

from yourflaskmodule import create_app
from yourflaskmodule.config import test_config
from yourflaskmodule import db as _db


@pytest.fixture(scope="session")
def app(request):
    """Test session-wide test `Flask` application."""
    app = create_app(test_config)
    return app


@pytest.fixture(autouse=True)
def _setup_app_context_for_test(request, app):
    """
    Given app is session-wide, sets up a app context per test to ensure that
    app and request stack is not shared between tests.
    """
    ctx = app.app_context()
    ctx.push()
    yield  # tests will run here
    ctx.pop()


@pytest.fixture(scope="session")
def db(app, request):
    """Returns session-wide initialized database"""
    with app.app_context():
        _db.create_all()
        yield _db
        _db.drop_all()


@pytest.fixture(scope="function")
def session(app, db, request):
    """Creates a new database session for each test, rolling back changes afterwards"""
    connection = _db.engine.connect()
    transaction = connection.begin()

    options = dict(bind=connection, binds={})
    session = _db.create_scoped_session(options=options)

    _db.session = session

    yield session

    transaction.rollback()
    connection.close()
    session.remove()

Here's an example of a base config class with the SQLite in-memory override:

class test_config(base_config):
    """Testing configuration options."""
    ENV_PREFIX = 'APP_'

    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///memory'

Here's an example of a test making use of all the different features:

import pytest

try:
    from flask import _app_ctx_stack as ctx_stack
except ImportError:
    from flask import _request_ctx_stack as ctx_stack

class TestCase:
    # set a Flask app config value using a pytest mark
    @pytest.mark.options(VERIFY_IDENTITY=True)
    def test_foo(self, client, session):
        # set user identity in app context
        ctx_stack.top.claims = {'sub': 'user1', 'tid': 'expected-audience'}

        # mock a class
        mocked_batch_client = mocker.patch('backend_class.BackendClient')
        assert(mocked_batch_client.return_value.list.return_value = ['a', 'b'])

        # test a view - it uses BackendClient (mocked now)
        resp = client.get('/api/items')
        data = json.loads(resp.data)
        assert(len(data['results']) > 0)

        # insert data into the database - will get rolled back after test completion
        item = YourModel()
        item.foo = "bar"
        session.add(item)
        session.commit()