import pytest import pytest_asyncio from fastapi.testclient import TestClient from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool from app.config import settings from app.db import Base, get_db from app.main import app # pytest-asyncio gives each test function its own event loop by default; # asyncpg connections are bound to the loop they were opened on, so a pooled # connection from one test's loop breaks the next test. NullPool sidesteps # this by opening a fresh connection per checkout — scoped to this dedicated # test engine so app.db.engine (used by production) is unaffected. test_engine = create_async_engine(settings.database_url, poolclass=NullPool) TestSessionLocal = async_sessionmaker(test_engine, expire_on_commit=False) @pytest_asyncio.fixture(autouse=True) async def _reset_db(): async with test_engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.create_all) yield async def _override_get_db(): async with TestSessionLocal() as session: yield session app.dependency_overrides[get_db] = _override_get_db @pytest_asyncio.fixture async def client(): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="https://test") as ac: yield ac @pytest.fixture def sync_client(monkeypatch): """Sync TestClient (supports WebSocket tests). The WS handler's session maker is swapped to the NullPool test engine so its writes land in the same test database the async fixtures see. The lifespan's engine is also swapped: TestClient runs the lifespan on its own portal loop, and the pooled production engine would carry connections across loops.""" import app.main as main_module import app.ws as ws_module monkeypatch.setattr(ws_module, "session_maker", TestSessionLocal) monkeypatch.setattr(main_module, "engine", test_engine) with TestClient(app, base_url="https://testserver") as tc: yield tc @pytest_asyncio.fixture async def db_session(): async with TestSessionLocal() as session: yield session