Pytest结合FastAPI测试时跨测试文件数据库未重置的问题
嘿,我来帮你捋捋这个问题!你现在碰到的情况是,每个测试文件里都写了数据库初始化的代码,但跨文件跑测试时,之前测试留下的数据没被清除,对吧?
问题根源
你当前用的是sqlite:///:memory:内存数据库,而且每个测试文件都重复初始化了引擎和表,但这里有个容易踩的坑:当Pytest在同一个进程中运行多个测试文件时,所有使用同一个URL创建的SQLAlchemy引擎会共享同一个内存数据库实例。也就是说,你在test_product.py里创建的Category数据,会留在这个共享的内存库中,后面的测试文件再用同样的引擎连接时,数据还在——哪怕你每个文件都调用了Base.metadata.drop_all和create_all,也没法完全隔离不同测试文件的数据库状态。再加上你用了StaticPool连接池,它会一直持有同一个数据库连接,进一步加剧了数据共享的问题。
解决方案
这里给你两种可行的解决思路,按需选择:
思路1:用Pytest Fixture规范管理数据库生命周期(推荐)
把数据库相关的初始化、销毁逻辑统一放到Pytest的conftest.py文件里(Pytest会自动识别这个文件里的fixture),通过fixture的作用域来控制数据库的隔离级别。
首先创建conftest.py文件,写入以下代码:
import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from fastapi.testclient import TestClient # 替换成你实际的应用模块导入 from your_app import app, get_db, Base @pytest.fixture(scope="module") # scope="module"表示每个测试文件(模块)用一个独立数据库 def db_engine(): # 创建全新的内存数据库引擎 engine = create_engine( "sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool ) # 创建所有表 Base.metadata.create_all(bind=engine) yield engine # 测试文件跑完后销毁所有表 Base.metadata.drop_all(bind=engine) @pytest.fixture(scope="module") def db_session(db_engine): # 创建测试用的数据库会话 TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=db_engine) session = TestingSessionLocal() try: yield session finally: session.close() @pytest.fixture(scope="module") def client(db_session): # 覆盖FastAPI的数据库依赖 def override_get_db(): try: yield db_session finally: db_session.close() app.dependency_overrides[get_db] = override_get_db # 返回测试客户端 yield TestClient(app) # 测试结束后清除依赖覆盖 del app.dependency_overrides[get_db]
然后你的测试文件(比如test_product.py)就可以直接使用这些fixture,不用再重复写数据库初始化代码了:
def test_create_category_for_test(client): payload = { "name": "camisa_time" } response = client.post("/categories", json=payload) # 你的断言逻辑... assert response.status_code == 200 assert response.json()["name"] == "camisa_time"
- 如果需要更严格的隔离(每个测试函数都用全新的数据库),只需要把fixture的
scope参数改成"function"即可。
思路2:修改SQLite连接URL实现独立内存库
如果你不想用fixture,也可以直接修改数据库连接URL,让每个测试文件创建的引擎都指向独立的内存数据库:
把你代码里的DB_URL改成"sqlite://"(去掉:memory:):
DB_URL = "sqlite://" # 替换原来的"sqlite:///:memory:" engine = create_engine(DB_URL, connect_args={"check_same_thread": False}, poolclass=StaticPool) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base.metadata.drop_all(bind=engine) Base.metadata.create_all(bind=engine) def override_get_db(): try: db = TestingSessionLocal() yield db finally: db.close() app.dependency_overrides[get_db] = override_get_db
这个方法的原理是:sqlite://会让SQLAlchemy为每个引擎创建一个全新的、独立的内存数据库,而不是复用之前的实例,这样不同测试文件的数据库就完全隔离了。
总结
核心问题就是多个测试文件共享了同一个内存数据库实例,不管用哪种方案,只要让每个测试文件(或测试函数)拥有独立的数据库实例,就能解决数据残留的问题。
备注:内容来源于stack exchange,提问作者João

