jeancochrane / pytest-flask-sqlalchemy

A pytest plugin for preserving test isolation in Flask-SQLAlchemy using database transactions.
MIT License
255 stars 45 forks source link

Detached instance errors - Instance is not bound to a session #27

Open JehoG opened 4 years ago

JehoG commented 4 years ago

In a test suite that has nearly 2000 test, sometimes there are some tests failing because we get an DetachedInstance exception withing the test (after a call to a endpoint we want to test):

sqlalchemy.orm.exc.DetachedInstanceError: Instance <Object at 0xxxxxxxxxxxx> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: http://sqlalche.me/e/bhk3)

+.venv/lib/python3.6/site-packages/sqlalchemy/orm/loading.py:915: DetachedInstanceError

This bug is unpredictible, doesn't happen when executing one test alone and when using IPDB before the call to the endpoint it wouldn't happen. After investigation, we realized that in some rare case, the db_session is closed by flask, so I guess that sometime this package fails to block a db_session closing.

Locally we got able to avoid it by removing the sqlalchemy shutdown_session function (which is set by flask-sqlalchemy) from app.teardown_appcontext_funcs and running it at the end of each test.

It's hard to reproduce, but here are some hints that I got able to spot: It happens only in a test when we fetch an object, make the call to the endpoint and then try to access the attribute of the object. Needs to be multiple test running, running only one doesn't cause any problem. Most of the case, the test before validate that an exception has been raised and handled within the tested endpoint. It happens more in a test that follows a test which is parametrized, by example: @pytest.mark.parametrize('expected_status', [429, 503, 504])

jshridha commented 4 years ago

@JehoG I'm running into the same issue with just 74 tests. Can you post an example of your workaround? I think it'll be helpful for others (and myself).

I added the following to my conftest.py and it seems to have fixed the issue:

@pytest.fixture(scope="function")
def db_session(db_session):
    with mock.patch.object(db_session, "remove", lambda: None):
        yield db_session
JehoG commented 4 years ago

@jshridha Sure, here's the fixtures we use to avoid this problem:

@pytest.fixture(scope='session')
def app():
    app = initialize_app(config_name='tests')

    # Here we backup the teardown function set by flask-sqlalchemy (it's closing the session)
    app.teardown_bkp = app.teardown_appcontext_funcs
    app.teardown_appcontext_funcs = []
    return app

To be sure that the session is closed at the end of each test:

def close_sessions(app, request):
    # At the end of each test we close the db_session
    @request.addfinalizer
    def teardown():
        flask_sa_shutdown_session_func = app.teardown_bkp[0]

It doesn't solves the problem since this package should be handling it, but we are able to avoid it this way.