【问题标题】:Isolating py.test DB sessions in Flask-SQLAlchemy在 Flask-SQLAlchemy 中隔离 py.test 数据库会话
【发布时间】:2015-02-15 13:53:51
【问题描述】:

我正在尝试使用 Flask-SQLAlchemy 构建一个 Flask 应用程序;我使用 pytest 来测试数据库。问题之一似乎是在不同测试之间创建了隔离的数据库会话。

我制作了一个最小的完整示例来突出问题,请注意test_user_schema1()test_user_schema2() 是相同的。

文件名: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

如果数据库在我的测试之间真正隔离,则两个测试都应该通过。但是,最后一次测试总是失败,因为我还没有找到让 db session 正确回滚的方法。

conftest.py 根据我在Alex Michael's blog post 中看到的内容使用以下内容,但是这个夹具代码中断,因为它显然没有隔离夹具之间的数据库会话。

@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()

出于这个问题的目的,我构建了 a gist,其中包含了重现它所需的所有内容;你可以用git clone https://gist.github.com/34fa8d274fc4be240933.git克隆它。

我正在使用以下软件包...

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

两个问题:

  1. 为什么会打破现状?同样的 py.test 固定装置似乎也适用于其他人。
  2. 如何解决此问题才能正常工作?

【问题讨论】:

    标签: python unit-testing flask flask-sqlalchemy pytest


    【解决方案1】:

    Alex Michael's blog post 中介绍的方法不工作,因为它不完整。根据sqlalchemy documentation on joining sessions,Alex 的解决方案只有在没有回滚调用的情况下才有效。另一个区别是,与 Alex 博客上的作用域会话相比,sqla 文档中使用了普通的 Session 对象。

    在 flask-sqlalchemy 的情况下,作用域会话会在 request teardown 上自动删除。调用session.remove,在后台发出回滚。要支持测试范围内的回滚,请使用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()
    

    您的数据库必须支持SAVEPOINT

    【讨论】:

    • 对于使用 sqlite 并希望使用这个的人stackoverflow.com/a/36457404/154607
    • 我一直在使用更幼稚的会话夹具并在被测代码中围绕提交来避免提交外部事务。这个答案让五年的挫败感化为乌有。
    • @TravisMehlinger 我之前也跳了一两年。很多时候我以为我阅读了 SA 文档,但仍然不知道该怎么做..
    • 对于像我一样困惑的其他人,这个函数之前应该有一个import sqlalchemy as sa
    • 为此感谢一百万。救生员。在我们的设置中,有一个测试失败,sqlalchemy.exc.ResourceClosedError: This Connection is closed。这是通过将原始 db.session 保存在临时变量中并在此函数结束时恢复它来解决的。我不确定发生了什么(这是一个试图访问用户数据库的身份验证包装器,所以它可能在测试包装器之前/之后运行?)
    【解决方案2】:

    1.

    根据Session Basics - SQLAlchemy documentation

    commit() 用于提交当前事务。 它总是事先发出 flush() 以将任何剩余状态刷新到数据库;这与“autoflush”设置无关。 ....

    所以会话夹具功能中的transaction.rollback() 没有生效,因为事务已经提交。


    2.

    将fixtures的范围改为function而不是session,这样每次都会清除db。

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

    顺便说一句,如果你使用内存中的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()
    

    【讨论】:

    • 关于第 1 点:这是我的困惑;我想测试 User.id 的 primary_key 是否正确写入,并且 auto_increments 符合我的预期。但是,除非我session.commit(),否则我真的不能这样做,对吧?抱歉,如果这是一个愚蠢的问题,我对 SQLAlchemy 有点陌生
    • @MikePennington,对不起,我不明白你的评论。这里是fork版本:gist.github.com/jeong-min-lee/28fd88babe2808afe9ba(只修改了conftest.py)和测试结果截图:i.imgur.com/LxZnsvw.png
    • @MikePennington,明确地说,我并不是要删除对 session.commit 的调用。
    • 谢谢,我试图避免scope=function,因为我不想花时间重新创建数据库,但是内存数据库非常快。我正在使用 sqlite 进行开发,可能会部署在 postgres 或 oracle 上。其他数据库(如 Oracle)的内存测试可能不起作用,但这已经足够了
    • @falsetru 关于回滚不生效,另见docs.sqlalchemy.org/en/latest/orm/…
    猜你喜欢
    • 1970-01-01
    • 2014-09-21
    • 2019-07-03
    • 2018-02-08
    • 2019-02-08
    • 1970-01-01
    • 1970-01-01
    • 2016-11-17
    • 2016-10-17
    相关资源
    最近更新 更多