简体   繁体   English

在 Flask-SQLAlchemy 中隔离 py.test 数据库会话

[英]Isolating py.test DB sessions in Flask-SQLAlchemy

I'm trying to build a Flask app with Flask-SQLAlchemy;我正在尝试使用 Flask-SQLAlchemy 构建 Flask 应用程序; I use pytest to test the DB.我使用 pytest 来测试数据库。 One of the problems seems to be creating isolated DB sessions between different tests.问题之一似乎是在不同测试之间创建隔离的数据库会话。

I cooked up a minimal, complete example to highlight the problem, note that test_user_schema1() and test_user_schema2() are the same.我编写了一个最小的、完整的示例来突出问题,注意test_user_schema1()test_user_schema2()是相同的。

Filename: test_db.py文件名: test_db.py

from models import User

def test_user_schema1(session):
    person_name = 'Fran Clan'
    uu = User(name=person_name)
    session.add(uu)
    session.commit()

    assert uu.id==1
    assert uu.name==person_name

def test_user_schema2(session):
    person_name = 'Stan Clan'
    uu = User(name=person_name)
    session.add(uu)
    session.commit()

    assert uu.id==1
    assert uu.name==person_name

If the db is truly isolated between my tests, both tests should pass.如果数据库在我的测试之间真正隔离,则两个测试都应该通过。 However, the last test always fails, because I haven't found a way to make db sessions rollback correctly.但是,最后一次测试总是失败,因为我还没有找到正确回滚数据库会话的方法。

sqlalchemy_session_fail

conftest.py uses the following based on what I saw in Alex Michael's blog post , but this fixture code breaks because it apparently doesn't isolate the db sessions between fixtures. conftest.py根据我在Alex Michael 的博客文章中看到的内容使用以下内容,但此夹具代码中断,因为它显然没有隔离夹具之间的数据库会话。

@pytest.yield_fixture(scope='function')
def session(app, db):
    connection = db.engine.connect()
    transaction = connection.begin()

    #options = dict(bind=connection, binds={})
    options = dict(bind=connection)
    session = db.create_scoped_session(options=options)

    yield session

    # Finalize test here
    transaction.rollback()
    connection.close()
    session.remove()

For the purposes of this question, I built a gist , which contains all you need to reproduce it;出于这个问题的目的,我构建了一个 gist ,其中包含重现它所需的所有内容; you can clone it with git clone https://gist.github.com/34fa8d274fc4be240933.git .你可以用git clone https://gist.github.com/34fa8d274fc4be240933.git克隆它。

I am using the following packages...我正在使用以下软件包...

Flask==0.10.1
Flask-Bootstrap==3.3.0.1
Flask-Migrate==1.3.0
Flask-Moment==0.4.0
Flask-RESTful==0.3.1
Flask-Script==2.0.5
Flask-SQLAlchemy==2.0
Flask-WTF==0.11
itsdangerous==0.24
pytest==2.6.4
Werkzeug==0.10.1

Two questions:两个问题:

  1. Why is status quo broken?为什么现状会被打破? This same py.test fixture seemed to work for someone else.这个相同的 py.test 固定装置似乎对其他人有用。
  2. How can I fix this to work correctly?我该如何解决这个问题才能正常工作?

The method introduced in Alex Michael's blog post is not working because it's incomplete. Alex Michael 的博客文章中介绍的方法不起作用,因为它不完整。 According to the sqlalchemy documentation on joining sessions , Alex's solution works only if there are no rollback calls.根据有关加入会话sqlalchemy 文档,Alex 的解决方案仅在没有回滚调用时才有效。 Another difference is, a vanilla Session object is used in sqla docs, compared to a scoped session on Alex's blog.另一个区别是,与 Alex 博客上的作用域会话相比,sqla 文档中使用了 vanilla Session对象。

In the case of flask-sqlalchemy, the scoped session is automatically removed on request teardown .在flask-sqlalchemy 的情况下,范围会话会在请求拆卸时自动删除。 A call to session.remove is made, which issues a rollback under the hood.调用session.remove会在session.remove发出回滚。 To support rollbacks within the scope of the tests, use SAVEPOINT :要支持测试范围内的回滚,请使用SAVEPOINT

import sqlalchemy as sa


@pytest.yield_fixture(scope='function')
def db_session(db):
    """
    Creates a new database session for a test. Note you must use this fixture
    if your test connects to db.

    Here we not only support commit calls but also rollback calls in tests.
    """
    connection = db.engine.connect()
    transaction = connection.begin()

    options = dict(bind=connection, binds={})
    session = db.create_scoped_session(options=options)

    session.begin_nested()

    # session is actually a scoped_session
    # for the `after_transaction_end` event, we need a session instance to
    # listen for, hence the `session()` call
    @sa.event.listens_for(session(), 'after_transaction_end')
    def restart_savepoint(sess, trans):
        if trans.nested and not trans._parent.nested:
            session.expire_all()
            session.begin_nested()

    db.session = session

    yield session

    session.remove()
    transaction.rollback()
    connection.close()

Your database must support SAVEPOINT though.不过,您的数据库必须支持SAVEPOINT

1. 1.

According to Session Basics - SQLAlchemy documentation :根据会话基础 - SQLAlchemy 文档

commit() is used to commit the current transaction. commit()用于提交当前事务。 It always issues flush() beforehand to flush any remaining state to the database;它总是预先发出 flush() 来将任何剩余状态刷新到数据库; this is independent of the “autoflush” setting.这与“自动冲洗”设置无关。 .... ....

So transaction.rollback() in session fixture function does not take effect, because the transaction is already committed.所以sessionfixture函数中的transaction.rollback()不会生效,因为事务已经提交了。


2. 2.

Change scope of fixtures to function instead of session so that db is cleared every time.将设备的范围更改为function而不是session以便每次都清除 db。

@pytest.yield_fixture(scope='function')
def app(request):
    ...

@pytest.yield_fixture(scope='function')
def db(app, request):
    ...

BTW, If you use in-memory sqlite database, you don't need to delete the db files, and it will be faster:顺便说一句,如果您使用内存中的sqlite数据库,则不需要删除db文件,而且速度会更快:

DB_URI = 'sqlite://'  # SQLite :memory: database

...

@pytest.yield_fixture(scope='function')
def db(app, request):
    _db.app = app
    _db.create_all()
    yield _db
    _db.drop_all()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM