0
votes

I am writing a Flask application that uses SQLAlchemy for its database backend.

The Flask application is created with an app factory called create_app.

from flask import Flask

def create_app(config_filename = None):
    app = Flask(__name__)
    if config_filename is None:
        app.config.from_pyfile('config.py', silent=True)
    else:
        app.config.from_mapping(config_filename)

    from .model import db
    db.init_app(app)
    db.create_all(app=app)

    return app

The database model consists of a single object called Document.

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Document(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    document_uri = db.Column(db.String, nullable=False, unique=True)

I am using pytest to do unit testing. I create a pytest fixture called app_with_documents that calls the application factory to create an application and adds some Document objects to the database before the test is run, then empties out the database after the unit test has completed.

import pytest
from model import Document, db
from myapplication import create_app

@pytest.fixture
def app():
    config = {
        'SQLALCHEMY_DATABASE_URI': f"sqlite:///:memory:",
        'TESTING': True,
        'SQLALCHEMY_TRACK_MODIFICATIONS': False
    }
    app = create_app(config)
    yield app
    with app.app_context():
        db.drop_all()

@pytest.fixture
def app_with_documents(app):
    with app.app_context():
        document_1 = Document(document_uri='Document 1')
        document_2 = Document(document_uri='Document 2')
        document_3 = Document(document_uri='Document 3')
        document_4 = Document(document_uri='Document 4')
        db.session.add_all([document_1, document_2, document_3, document_4])
        db.session.commit()
    return app

I have multiple unit tests that use this fixture.

def test_unit_test_1(app_with_documents):
    ...

def test_unit_test_2(app_with_documents):
    ...

If I run a single unit test everything works. If I run more than one test, subsequent unit tests crash at the db.session.commit() line in the test fixture setup with "no such table: document".

    def do_execute(self, cursor, statement, parameters, context=None):
>       cursor.execute(statement, parameters)
E       sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: document [SQL: 'INSERT INTO document (document_uri) VALUES (?)'] [parameters: ('Document 1',)] (Background on this error at: http://sqlalche.me/e/e3q8)

What I expect is that each unit test gets its own brand-new identical prepopulated database so that all the tests would succeed.

(This is an issue with the database tables, not the unit tests. I see the bug even if my unit tests consist of just pass.)

The fact that the error message mentions a missing table makes it look like the db.create_all(app=app) in create_app is not being called after the first unit test runs. However, I have verified in the debugger that this application factory function is called once for every unit test as expected.

It is possible that my call to db.drop_all() is an incorrect way to clear out the database. So instead of an in-memory database, I tried creating one on disk and then deleting it as part of the test fixture cleanup. (This is the technique recommended in the Flask documentation.)

@pytest.fixture
def app():
    db_fd, db_filename = tempfile.mkstemp(suffix='.sqlite')
    config = {
        'SQLALCHEMY_DATABASE_URI': f"sqlite:///{db_filename}",
        'TESTING': True,
        'SQLALCHEMY_TRACK_MODIFICATIONS': False
    }
    yield create_app(config)
    os.close(db_fd)
    os.unlink(db_filename)

This produces the same error.

  • Is this a bug in Flask and/or SQLAlchemy?
  • What is the correct way to write Flask test fixtures that prepopulate an application's database?

This is Flask 1.0.2, Flask-SQLAlchemy 2.3.2, and pytest 3.6.0, which are all the current latest versions.

1

1 Answers

0
votes

In my conftest.py I was importing the contents of model.py in my application like so.

from model import Document, db

I was running the unit tests in Pycharm using Pycharm's pytest runner. If instead I run tests from the command line with python -m pytest I see the following error

ModuleNotFoundError: No module named 'model'
ERROR: could not load /Users/wmcneill/src/FlaskRestPlus/test/conftest.py

I can get my tests running from the command line by fully-qualifying the import path in conftest.py.

from myapplication.model import Document, db

When I do this all the unit tests pass. They also pass when I run the unit tests from inside Pycharm.

So it appears that I had incorrectly written an import statement in my unit tests. However, when I ran those unit tests via Pycharm, instead of seeing an error message about the import, the scripts launched but then had weird SQL errors.

I still haven't figured out why I saw the strange SQL errors I did. Presumably something subtle about the way global state is being handled. But changing the import line fixes my problem.