You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Pytest结合FastAPI测试时跨测试文件数据库未重置的问题

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.16 11:54:35