Open andredias opened 7 years ago
+1
Confusing docs.
+1
I got something working, thanks to http://alexmic.net/flask-sqlalchemy-pytest/
Here's my code:
import os
import pytest
from myapp import create_app, db as _db
@pytest.fixture(scope='session')
def app():
app = create_app()
app.config.from_object('test_settings')
return app
@pytest.fixture(scope='session')
def db(app, request):
if os.path.exists(app.config['DB_PATH']):
os.unlink(app.config['DB_PATH'])
def teardown():
_db.drop_all()
os.unlink(app.config['DB_PATH'])
_db.init_app(app)
_db.create_all()
request.addfinalizer(teardown)
return _db
After much searching and hair-tearing, I found that the current approach works quite nicely:
# module conftest.py
import pytest
from app import create_app
from app import db as _db
from sqlalchemy import event
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope="session")
def app(request):
"""
Returns session-wide application.
"""
return create_app("testing")
@pytest.fixture(scope="session")
def db(app, request):
"""
Returns session-wide initialised database.
"""
with app.app_context():
_db.drop_all()
_db.create_all()
@pytest.fixture(scope="function", autouse=True)
def session(app, db, request):
"""
Returns function-scoped session.
"""
with app.app_context():
conn = _db.engine.connect()
txn = conn.begin()
options = dict(bind=conn, binds={})
sess = _db.create_scoped_session(options=options)
# establish a SAVEPOINT just before beginning the test
# (http://docs.sqlalchemy.org/en/latest/orm/session_transaction.html#using-savepoint)
sess.begin_nested()
@event.listens_for(sess(), 'after_transaction_end')
def restart_savepoint(sess2, trans):
# Detecting whether this is indeed the nested transaction of the test
if trans.nested and not trans._parent.nested:
# The test should have normally called session.commit(),
# but to be safe we explicitly expire the session
sess2.expire_all()
sess.begin_nested()
_db.session = sess
yield sess
# Cleanup
sess.remove()
# This instruction rollsback any commit that were executed in the tests.
txn.rollback()
conn.close()
The key here is to run your tests within a nested session, and then rollback everything after the execution of each test (this also assumes there are no dependencies across your tests).
@kenshiro-o it would be great if you could open a PR adding this to the docs somewhere. 😁
@nicoddemus sure! Will do so during the week :smile_cat:
Hi @kenshiro-o , Have you ever tried using SQLAlchemy fixture + Factoryboy? I was wondering how Factoryboy could obtain an instance of your session fixture. This is the default example in the documentation, but doesn't seems to work when testing:
from sqlalchemy import Column, Integer, Unicode, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
engine = create_engine('sqlite://')
session = scoped_session(sessionmaker(bind=engine))
Base = declarative_base()
class User(Base):
""" A SQLAlchemy simple model class who represents a user """
__tablename__ = 'UserTable'
id = Column(Integer(), primary_key=True)
name = Column(Unicode(20))
Base.metadata.create_all(engine)
import factory
class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = User
sqlalchemy_session = session # Here needs the SQLAlchemy session object!!!!!!
id = factory.Sequence(lambda n: n)
name = factory.Sequence(lambda n: u'User %d' % n)
I tried updating the factories during session creation and it actually works:
@pytest.fixture(scope="function", autouse=True)
def session(app, db, request):
"""
Returns function-scoped session.
"""
with app.app_context():
conn = _db.engine.connect()
txn = conn.begin()
options = dict(bind=conn, binds={})
sess = _db.create_scoped_session(options=options)
# establish a SAVEPOINT just before beginning the test
# (http://docs.sqlalchemy.org/en/latest/orm/session_transaction.html#using-savepoint)
sess.begin_nested()
@event.listens_for(sess(), 'after_transaction_end')
def restart_savepoint(sess2, trans):
# Detecting whether this is indeed the nested transaction of the test
if trans.nested and not trans._parent.nested:
# The test should have normally called session.commit(),
# but to be safe we explicitly expire the session
sess2.expire_all()
sess.begin_nested()
_db.session = sess
UserFactory._meta.sqlalchemy_session = sess # THIS WILL DO THE MAGIC
yield sess
# Cleanup
sess.remove()
# This instruction rollsback any commit that were executed in the tests.
txn.rollback()
conn.close()
but I was wondering if some of you guys know a more "elegant" solution.
@AnderUstarroz I encountered the same obstacle in the face factories not using the session, and overcome it with a bit more flexible solution:
from . import factories
for name in dir(factories):
item = getattr(factories, name)
if isinstance(item, factories.factory.base.FactoryMetaClass):
item._meta.sqlalchemy_session = session
This works with SQLAlchemy + Factoryboy:
@pytest.fixture
def app():
app = make_app('test')
with app.app_context():
db.create_all()
yield app
db.session.remove()
db.drop_all()
Then you may use db.session
normally in factory Meta
. My tests run against in-memory SQLite.
Hi, there Could you include a SQLAlchemy example in the documentation? I'm using the following code but it doesn't work:
This is the error that I get: