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()