Async tests with mocked DB (SQLAlchemy async)
Async tests with mocked DB (SQLAlchemy async)
Async tests with mocked DB (SQLAlchemy async)
- SQLAlchemy 2.0 async engine
- Async session override for FastAPI dependency
- pytest + pytest-asyncio
- httpx.AsyncClient
- In-memory SQLite database for testing
You can copy/paste this into your project.
Project Structure
app/
├── main.py
├── database.py
├── models.py
├── routers.py (optional)
tests/
└── test_users_async_db.py1. SQLAlchemy Async Setup (database.py)
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker, declarative_base
DATABASE_URL = "sqlite+aiosqlite:///./prod.db"
engine = create_async_engine(
DATABASE_URL, echo=False, future=True
)
async_session_maker = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
Base = declarative_base()
async def get_async_session():
async with async_session_maker() as session:
yield session2. Model (models.py)
from sqlalchemy.orm import mapped_column, Mapped
from sqlalchemy import String
from app.database import Base
class User(Base):
__tablename__ = "users"
username: Mapped[str] = mapped_column(String, primary_key=True)
job: Mapped[str] = mapped_column(String)3. FastAPI App With Async DB Dependency (main.py)
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_async_session
from app.models import User
app = FastAPI()
@app.get("/users/{username}")
async def get_user(username: str, session: AsyncSession = Depends(get_async_session)):
result = await session.execute(select(User).where(User.username == username))
user = result.scalars().first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return {"username": user.username, "job": user.job}
@app.post("/users")
async def create_user(
username: str,
job: str,
session: AsyncSession = Depends(get_async_session),
):
# check existing
result = await session.execute(select(User).where(User.username == username))
if result.scalars().first():
raise HTTPException(status_code=400, detail="User already exists")
new_user = User(username=username, job=job)
session.add(new_user)
await session.commit()
return {"message": "User created", "username": username}4. Async Test DB + Dependency Override (test_users_async_db.py)
Uses an in-memory SQLite DB for isolated tests.
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_async_session
from app.models import User
# Create TEST DB (in memory)
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
test_engine = create_async_engine(
TEST_DATABASE_URL, echo=False, future=True
)
TestSessionLocal = sessionmaker(
test_engine, expire_on_commit=False, class_=AsyncSession
)
# Dependency override for tests
async def override_get_async_session():
async with TestSessionLocal() as session:
yield session
app.dependency_overrides[get_async_session] = override_get_async_session
# Create DB schema before tests
@pytest.fixture(scope="module", autouse=True)
async def prepare_database():
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
# -----------------------------------------
# TESTS
# -----------------------------------------
@pytest.mark.asyncio
async def test_create_user():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post("/users", params={"username": "alice", "job": "Engineer"})
assert response.status_code == 200
assert response.json()["message"] == "User created"
@pytest.mark.asyncio
async def test_get_user():
# Insert user manually into mocked DB for this test
async with TestSessionLocal() as session:
session.add(User(username="bob", job="Designer"))
await session.commit()
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/users/bob")
assert response.status_code == 200
assert response.json() == {"username": "bob", "job": "Designer"}
@pytest.mark.asyncio
async def test_missing_user():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/users/unknown")
assert response.status_code == 404
assert response.json() == {"detail": "User not found"}How this works
✔ Uses in-memory async SQLite
Fast for tests, isolated from production.
✔ Dependency override
FastAPI's Depends(get_async_session) is replaced with the test session.
✔ No real database I/O
Everything runs in RAM.
✔ Tests are fully asynchronous
pytest.mark.asyncio ensures async execution works correctly.