Test-Engine pro Test statt Modul-Global erzeugen (echter Fix für Event-Loop-Konflikt)
CI / backend-tests (push) Failing after 28s
CI / backend-tests (push) Failing after 28s
Der vorherige Fix (asyncio_default_fixture_loop_scope/asyncio_default_test_loop_scope) griff nicht - letztere Option existiert in der gepinnten pytest-asyncio-Version vermutlich noch nicht, Tests liefen weiterhin auf function-scoped Loops während die Engine session-weit global war. Echte Ursache behoben: db_session erzeugt jetzt eine frische AsyncEngine pro Test (und disposed sie danach), sodass Engine/Pool immer auf derselben Loop laufen wie der Test selbst. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
+27
-24
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
|
||||
from app.core.app_settings import settings
|
||||
from app.core.security import hash_password
|
||||
@@ -11,35 +11,38 @@ from app.models.auth import Benutzer, BenutzerRolle, RolleTyp
|
||||
from app.models.objekt import Objekt
|
||||
from app.models.stammdaten import Bereich, Objekttyp, Standort
|
||||
|
||||
# Erwartet eine bereits per Alembic migrierte Test-Datenbank (CI: install -> migrate -> pytest,
|
||||
# siehe testphasen.md Phase 0). Jeder Test läuft in einer Transaktion, die am Ende zurückgerollt
|
||||
# wird, damit Tests sich nicht gegenseitig beeinflussen.
|
||||
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
|
||||
TestSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def db_session():
|
||||
"""Jeder Test läuft in einer äußeren Transaktion, die am Ende zurückgerollt wird.
|
||||
|
||||
Die Session ist per `join_transaction_mode="create_savepoint"` an die äußere
|
||||
Connection gebunden: ein `commit()` innerhalb des Tests (z. B. durch `get_db`,
|
||||
das jetzt selbst committet) schließt nur eine SAVEPOINT ab, nicht die äußere
|
||||
Transaktion – das äußere `connection.rollback()` verwirft am Ende trotzdem alles.
|
||||
(SQLAlchemy 2.0 "Joining a Session into an External Transaction" – Review-Fund
|
||||
fastapi-expert: ohne dieses Pattern hätte ein commit() in Endpunkten die
|
||||
Testisolation gebrochen.)
|
||||
Die Engine wird bewusst PRO TEST neu erzeugt (nicht als Modul-Global): asyncpg-
|
||||
Verbindungen sind an die Event-Loop gebunden, in der sie entstanden – ein
|
||||
session-weit wiederverwendeter Pool kollidierte mit pytest-asyncios (Default)
|
||||
function-scoped Loop pro Test ("attached to a different loop" / "another
|
||||
operation is in progress", Fund im zweiten echten CI-Lauf). Mit einer frischen
|
||||
Engine pro Test entsteht der Pool immer auf der Loop, die auch den Test ausführt.
|
||||
|
||||
Die Session ist zusätzlich per `join_transaction_mode="create_savepoint"` an die
|
||||
äußere Connection gebunden: ein `commit()` innerhalb des Tests (z. B. durch
|
||||
`get_db`, das jetzt selbst committet) schließt nur eine SAVEPOINT ab, nicht die
|
||||
äußere Transaktion – das äußere `connection.rollback()` verwirft am Ende trotzdem
|
||||
alles (SQLAlchemy 2.0 "Joining a Session into an External Transaction").
|
||||
"""
|
||||
async with engine.connect() as connection:
|
||||
await connection.begin()
|
||||
session = AsyncSession(
|
||||
bind=connection, expire_on_commit=False, join_transaction_mode="create_savepoint"
|
||||
)
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
await connection.rollback()
|
||||
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
|
||||
try:
|
||||
async with engine.connect() as connection:
|
||||
await connection.begin()
|
||||
session = AsyncSession(
|
||||
bind=connection, expire_on_commit=False, join_transaction_mode="create_savepoint"
|
||||
)
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
await connection.rollback()
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
|
||||
Reference in New Issue
Block a user