🚀 AsyncSession × SQLModel × Pytest 実践ガイド:非同期DBテストの正しい作り方
🌐 はじめに
FastAPIを本格的に使い始めると、async def による非同期エンドポイントと AsyncSession(非同期DBセッション)を組み合わせた構成が主流になります。これは、高い同時処理性能とレスポンス性能を実現できる一方で、設計やテストの難易度を大きく引き上げる要因にもなります。
特に、非同期DBを使ったAPIのテストでは、同期版では意識しなくてよかった問題が顕在化します。
- AsyncSession の初期化方法が分かりにくい
- テスト実行時にイベントループが衝突する
- DBの初期化やクリーンアップの責務が曖昧になる
- CI環境でだけテストが失敗する
本記事では、AsyncSession × SQLModel × Pytest を組み合わせたときの「壊れにくく、再現性の高い正しい構成」を、実務でそのまま流用できる形で体系的に解説します。
🧩 使用技術と前提
本記事で扱う技術スタックは以下の通りです。
- FastAPI:非同期処理を前提としたモダンWebフレームワーク
- SQLModel:PydanticとSQLAlchemyを統合したORM
- AsyncSession:SQLAlchemyの非同期セッション
- Pytest:Python標準のテストフレームワーク
- pytest-asyncio / anyio:非同期テストを安定させるための補助ライブラリ
- SQLite(aiosqlite):テスト用の軽量な非同期DB
前提として、以下の内容を理解していることを想定しています。
- FastAPIでの基本的なAPI作成経験
- Pytestでの基本的なテスト記述
- SQLModel(同期版)の利用経験
同期DBテストとの違いを意識しながら読み進めると理解が深まります。
🗂️ ディレクトリ構成
まずは、非同期DBテストを前提としたディレクトリ構成を確認します。
project/
├── app/
│ ├── main.py
│ ├── database.py
│ ├── models.py
│ └── routers/
│ └── items.py
├── tests/
│ ├── conftest.py
│ └── test_items_async.py
└── requirements.txt
この構成のポイントは、
- DB接続ロジックを database.py に完全に分離
- FastAPI依存関係として Session を注入
- tests 側から依存関係を差し替え可能
という点にあります。これにより、本番コードを変更せずにテスト環境だけを柔軟に構築できます。
🛢️ SQLModel + AsyncSession のDB設定
次に、AsyncSessionを使ったDB設定を行います。
from sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "sqlite+aiosqlite:///./app.db"
engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False,
)
async def get_session() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session
expire_on_commit=False を指定することで、commit後もオブジェクトが無効化されず、APIレスポンスでそのまま利用できます。これはテストだけでなく、本番コードでも重要な設定です。
🧪 AsyncSession対応のモデル定義
モデル定義自体は同期版とほぼ同じですが、非同期環境でも問題なく使える点がSQLModelの大きな利点です。
from sqlmodel import SQLModel, Field
class Item(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
price: int
このモデルは、
- APIリクエストボディ
- DBテーブル定義
- テストデータ構造
のすべてを1つで兼ねるため、非同期テストでも扱いやすくなります。
🔄 非同期CRUDエンドポイント
次に、AsyncSessionを使ったCRUDエンドポイントを定義します。
from fastapi import APIRouter, Depends
from sqlmodel.ext.asyncio.session import AsyncSession
from app.database import get_session
from app.models import Item
router = APIRouter()
@router.post("/items/")
async def create_item(item: Item, session: AsyncSession = Depends(get_session)):
session.add(item)
await session.commit()
await session.refresh(item)
return item
非同期CRUDでは、
await session.commit()await session.refresh()
を忘れると、テスト時に意図しない挙動を引き起こすため注意が必要です。
🧪 テスト用Async DBをfixtureで構築
ここが非同期DBテストの最重要ポイントです。テスト専用のDBとイベントループを fixture で完全に管理します。
import pytest
from sqlmodel import SQLModel
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlmodel.ext.asyncio.session import AsyncSession
@pytest.fixture(scope="session")
def anyio_backend():
return "asyncio"
@pytest.fixture
async def async_session():
engine = create_async_engine("sqlite+aiosqlite://", echo=False)
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with AsyncSessionLocal() as session:
yield session
await engine.dispose()
この構成により、
- テストごとに独立したDB
- イベントループの競合回避
- テスト終了後の確実なリソース解放
が保証されます。
🔁 FastAPI依存関係のオーバーライド(Async版)
FastAPIの依存関係オーバーライドを使い、テスト用AsyncSessionを注入します。
from fastapi.testclient import TestClient
from app.main import app
from app.database import get_session
@pytest.fixture
def client(async_session):
async def override_get_session():
yield async_session
app.dependency_overrides[get_session] = override_get_session
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
この方法により、本番DBに一切触れず、非同期APIを同期的にテストできます。
🧪 非同期CRUDテスト
最後に、非同期CRUD APIに対するテスト例を示します。
def test_create_item(client):
response = client.post(
"/items/",
json={"name": "Async Apple", "price": 200}
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "Async Apple"
assert data["price"] == 200
テストではDBの内部状態を直接検証するのではなく、APIの入出力に集中することが、保守性の高い設計につながります。
⚠️ AsyncSessionテストでよくある罠
-
pytest.mark.asyncioとTestClientを混在させない - セッションをテスト間で使い回しすぎない
-
expire_on_commit=Falseを設定し忘れない - engine を fixture の外で共有しない
- CI環境でイベントループ設定が異なる点に注意する
これらは、非同期DBテストで頻発するトラブルの原因です。
✅ まとめ
本記事では、AsyncSessionを使ったFastAPIアプリを 安全・高速・再現性高くテストするための実践構成 を解説しました。
- AsyncSession + SQLModel の正しい初期化方法
- 非同期DBを fixture で完全に分離する設計
- FastAPI依存関係の Async オーバーライド
- 実務で壊れにくい非同期CRUDテストの書き方
株式会社ONE WEDGE
【Serverlessで世の中をもっと楽しく】 ONE WEDGEはServerlessシステム開発を中核技術としてWeb系システム開発、AWS/GCPを利用した業務システム・サービス開発、PWAを用いたモバイル開発、Alexaスキル開発など、元気と技術力を武器にお客様に真摯に向き合う価値創造企業です。
Discussion