Flask-SQLAlchemy 3中如何在pytest里处理数据库会话?
我正在从Flask-SQLAlchemy 2迁移到3版本,了解到Flask-SQLAlchemy 3的会话是作用域绑定到应用上下文的——每次调用with app.app_context():都会创建新会话。比如在pytest中执行以下代码时:
db = flask_sqlalchemy.SQLAlchemy() app = create_app() db.init_app(app) with app.app_context(): db.session.add(new_user) db.session.flush() with app.app_context(): User.query.filter(User.id == new_user.id).first()
这段代码无法正常工作,因为第一个应用上下文关闭时会结束对应的会话,导致之前flush的数据丢失。
这让我对测试场景(比如使用Flask测试客户端)的处理方式感到困惑:调用client.post这类方法时,测试客户端会开启新的应用上下文,只能访问已提交到数据库的数据,而非仅flush的数据。
因此我认为,若要为测试植入初始数据(如供测试客户端使用的管理员用户),需要将数据提交到测试数据库,同时在每个测试后清理数据库。我编写了如下pytest fixture代码:
@pytest.fixture(scope="session") def app() -> Generator[Flask, Flask, Flask]: """Create a Flask app instance for testing.""" _app = create_app() yield _app @pytest.fixture(scope="session") def db(app: Flask) -> Generator["SQLAlchemy", "SQLAlchemy", "SQLAlchemy"]: """Instantiate a testing database.""" with app.app_context(): _db.init_app(app) yield _db with app.app_context(): _db.session.close_all() @pytest.fixture(scope="function") def session(app_context: Flask, db: "SQLAlchemy") -> Generator[Session, Session, Session]: _db.create_all() yield db.session _db.session.close_all() db.drop_all() @pytest.fixture(scope="function") def make_admin_user(session: Session): """Defines a fixture for the 'All' role.""" admin_user = build_admin_user() session.add(admin_user) session.commit(admin_user) def my_test(app): client = app.test_client() # Admin user credentials would work here because the admin user should be available # even in the new context that gets opened client.post("/login")
请问这个方案是否合理?我的思路是否正确?官方文档仅提到测试中应避免使用全局上下文以避免会话清理问题,未详细说明测试场景的处理方式。
你的思路完全正确,这个测试方案整体是合理的,适配了Flask-SQLAlchemy 3的会话绑定特性:
核心逻辑匹配版本特性
你明确了测试客户端会开启新应用上下文,因此必须提交数据才能跨上下文访问,这完全符合Flask-SQLAlchemy 3的会话生命周期规则——未提交的flush数据仅存在于当前会话/上下文,上下文销毁后数据会回滚。通过session.commit()提交管理员用户,确保测试客户端的新上下文能读取到数据,这个处理是准确的。Fixture设计的合理性
- 会话级的
app和dbfixture:避免重复创建应用和数据库实例,提升测试效率,符合pytest fixture的最佳实践。 - 函数级的
sessionfixture:每个测试前创建表、后销毁表,保证测试隔离性,防止测试间的数据污染。 make_admin_userfixture绑定到函数级,确保每个测试都能拿到干净的管理员数据,同时不会影响其他测试。
- 会话级的
可优化的细节
sessionfixture中,_db.create_all()和db.drop_all()需要在应用上下文内执行,当前代码可能存在上下文缺失的问题,建议修改为:@pytest.fixture(scope="function") def session(app: Flask, db: "SQLAlchemy") -> Generator[Session, Session, Session]: with app.app_context(): db.create_all() yield db.session db.session.close_all() db.drop_all()make_admin_user中的session.commit(admin_user)是错误的,commit()方法不需要传入参数,应该改为session.commit()。- 可以考虑使用事务回滚替代
drop_all(),提升测试速度:在每个测试后回滚事务而不是销毁表,适合表结构固定的场景:@pytest.fixture(scope="function") def session(app: Flask, db: "SQLAlchemy") -> Generator[Session, Session, Session]: with app.app_context(): db.create_all() session = db.session try: yield session finally: session.rollback() db.session.close_all() # 若需彻底清理可保留drop_all,否则仅回滚即可
总的来说,你的核心思路完全适配Flask-SQLAlchemy 3的特性,方案是可行的,调整上述细节后会更健壮。
内容的提问来源于stack exchange,提问作者kensentor

