I've met some problem with running tests using FastAPI+SQLAlchemy and PostgreSQL, which leads to lots of errors (however, it works well on SQLite). I've created a repo with MVP app and Pytest on Docker Compose testing.
The basic error is sqlalchemy.exc.InterfaceError('cannot perform operation: another operation is in progress'). This may be related to the app/DB initialization, though I checked that all the operations get performed sequentially. Also I tried to use single instance of TestClient for the all tests, but got no better results. I hope to find a solution, a correct way for testing such apps 🙏
Here are the most important parts of the code:
app.py:
app = FastAPI()
some_items = dict()
@app.on_event("startup")
async def startup():
await create_database()
# Extract some data from env, local files, or S3
some_items["pi"] = 3.1415926535
some_items["eu"] = 2.7182818284
@app.post("/{name}")
async def create_obj(name: str, request: Request):
data = await request.json()
if data.get("code") in some_items:
data["value"] = some_items[data["code"]]
async with async_session() as session:
async with session.begin():
await create_object(session, name, data)
return JSONResponse(status_code=200, content=data)
else:
return JSONResponse(status_code=404, content={})
@app.get("/{name}")
async def get_connected_register(name: str):
async with async_session() as session:
async with session.begin():
objects = await get_objects(session, name)
result = []
for obj in objects:
result.append({
"id": obj.id, "name": obj.name, **obj.data,
})
return result
tests.py:
@pytest.fixture(scope="module")
def event_loop():
loop = asyncio.get_event_loop()
yield loop
loop.close()
@pytest_asyncio.fixture(scope="module")
@pytest.mark.asyncio
async def get_db():
await delete_database()
await create_database()
@pytest.mark.parametrize("test_case", test_cases_post)
def test_post(get_db, test_case):
with TestClient(app)() as client:
response = client.post(f"/{test_case['name']}", json=test_case["data"])
assert response.status_code == test_case["res"]
@pytest.mark.parametrize("test_case", test_cases_get)
def test_get(get_db, test_case):
with TestClient(app)() as client:
response = client.get(f"/{test_case['name']}")
assert len(response.json()) == test_case["count"]
db.py:
DATABASE_URL = environ.get("DATABASE_URL", "sqlite+aiosqlite:///./test.db")
engine = create_async_engine(DATABASE_URL, future=True, echo=True)
async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
Base = declarative_base()
async def delete_database():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
async def create_database():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
class Model(Base):
__tablename__ = "smth"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
data = Column(JSON, nullable=False)
idx_main = Index("name", "id")
async def create_object(db: Session, name: str, data: dict):
connection = Model(name=name, data=data)
db.add(connection)
await db.flush()
async def get_objects(db: Session, name: str):
raw_q = select(Model) \
.where(Model.name == name) \
.order_by(Model.id)
q = await db.execute(raw_q)
return q.scalars().all()
"sqlite+aiosqlite:///./test.db"as your default connection string. I understand you're saying this (usingaiosqliteas the database driver library) works. What database driver library are you using with postgres when it fails -asyncpgor something? In other words, do you have something likepostgresql+asyncpg://name:pass@host:port/dbnamefor your$DATABASE_URL? It feels a bit like the error you're getting might be due to using a driver that doesn't support asynchronous calls...asyncpg, you can find the con.str in the GitHub repo. Also, I should mention that usual running of the app (without pytest) on Postgres works well, the app keeps all the data between different runs. Only the test setup seems to be wrong...pytest-asynciomyself, which I guess is where the issue lies. Wish I could be more helpful! You may have tried this, but if it were me I would focus on reconfiguringget_db- maybe for the tests it doesn't need to be async? Perhaps some standardpytestdebugging stuff like changing the fixture scope back to per-function, or sticking ayieldat the end ofget_dbmight bear fruit... :/create_databaseis an async func, just like all the CRUD, so I don't think thatget_dbcan and should be be sync 🤔 Also, this function must be module-scoped so that the data is persistent and test cases are ordered (tests ofgetuse data filled by tests ofpost). Anyway, thanks for the reply!get_dbto each function, and copy the 'post' into the second test definition before the 'get'? That seems a quick way to test whether this is the cause of the problem.