• 3 min read
  • 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()