金色の配線パターンが広がる基板の接写
ニュース深掘り

FastAPIのテストがDBに汚染される落とし穴と分離の対処法

目次を見る

複数のログソースを束ねるバックエンドAPIを開発していて、テストが時々失敗する、という経験はないでしょうか。

特にFastAPI(Python製の非同期Webフレームワーク)とPostgreSQLを組み合わせた構成では、テストが開発用DBに接続されたまま運用されているケースが珍しくありません。

この記事では、複数ログソース(Nginx、Kubernetes、Docker、アプリケーションログ、GitHub Actions)を取り込むインシデント調査ツールの開発事例をもとに、テストDBが汚染される典型的な落とし穴と、その切り分け方・対策を整理します。

何が起きるか: 空のはずのコレクションに古いデータが混入する

あるAPI開発プロジェクトでは、ログ取り込みAPI(POST /api/v1/logsやPOST /api/v1/logs/batchなど)の動作確認を手動のライブチェックで行っていました。

単発投入・バッチ投入ともに201 Createdや200 OKが返り、一見問題なく動いているように見えました。

ところが自動テストを実行すると、「空のコレクションが返るはず」のテストで6件のレコードが返ってくるという失敗が発生しました。

この手の失敗は厄介です。コードのロジック自体は正しいのに、テストだけが不安定に落ちる状態になります。CIパイプライン(継続的インテグレーションの自動実行基盤)で再実行すると通ってしまうこともあり、見逃されがちです。

なぜ起きるか: ライブチェックとテストが同じDBを見ている

原因を段階的に分解すると、根っこにあるのは「手動のAPI動作確認」と「pytestによる自動テスト」が同一の開発用データベースに対して実行されていたことです。

まず、ライブチェックでcurlやHTTPクライアントからPOST /api/v1/logsを叩くと、そのレコードは開発用DB(例ではincidentcopilotのようなDB)にそのまま残ります。

次に、自動テストが「初期状態は空である」という前提でアサーション(期待値の検証)を書いていると、ライブチェックの残骸がそのままテスト結果に混入します。

さらに根本的な問題として、テストごとにデータをクリーンアップする仕組み(トランザクションのロールバックやフィクスチャによる後始末)が用意されていなかった点も挙げられます。

FastAPIはDI(依存性注入、Dependency Injectionの略で処理に必要な部品を外部から差し込む仕組み)を使ってDB接続をエンドポイントに渡す設計が一般的です。

このDIを本番・開発用のDBセッションのまま使い続けると、テスト実行時も同じ接続先を参照してしまい、テストの独立性が保てなくなります。

自分のプロジェクトが該当するか確認する方法

まず、テスト実行時にどのDBへ接続しているかを確認します。

# .env や環境変数でDB接続先を確認
grep -i database_url .env
echo $DATABASE_URL

次に、pytestの設定ファイル(conftest.pyやpytest.ini)にテスト専用のDB接続やフィクスチャが定義されているかを見ます。

# テスト用DBへの切り替え処理があるか確認
grep -rn "test" backend/tests/conftest.py
grep -rn "override" backend/tests/conftest.py

conftest.py内にapp.dependency_overridesのような記述がなく、本番用のDBセッション生成関数をそのままインポートしているだけであれば、テストが開発用DBを直撃している可能性が高いです。

あわせて、Alembic(SQLAlchemy向けのマイグレーション管理ツール)のマイグレーション履歴が、テスト用DBにも個別に適用されているかも確認してください。

alembic current
alembic history

テスト用DBが存在せず、マイグレーション履歴も開発用DBと共有されている場合は、分離対応が未実施と判断してよいでしょう。

対策の手順: テスト専用DB + ロールバックフィクスチャ

対策の基本方針は「環境の分離」と「テストごとの状態の分離」の2段構えです。

まず環境の分離として、開発用DBとは別にテスト専用のDB(例: incidentcopilot_test)を作成します。

createdb incidentcopilot_test
DATABASE_URL=postgresql://user:pass@localhost/incidentcopilot_test alembic upgrade head

次に、backend/tests/conftest.pyにpytestフィクスチャを定義し、テスト専用のSQLAlchemyエンジンとセッションを用意します。

ポイントは、FastAPIの依存性注入をオーバーライド(上書き)して、アプリ全体がテスト用セッションを見るようにする点です。

テストごとの状態の分離としては、各テストの実行後にトランザクションをロールバックする仕組みを組み込みます。

これにより、あるテストで投入したデータが次のテストに漏れ出すことがなくなります。

ライブチェック(手動のAPI疎通確認)と自動テストは別物として扱い、接続先DBも分離するのが安全です。

実際にこの対策を適用した事例では、修正前は1件のテストが不安定に失敗していましたが、修正後は27件のテストが連続して安定してパスするようになりました。

さらに確認すべき点として、Alembicのマイグレーションに差分がないか(alembic check相当の確認)、Gitの差分に不要な空白変更が混ざっていないかもあわせてチェックしておくと安心です。

# マイグレーションに新しい差分が出ていないか確認
alembic check

# 不要な差分が混ざっていないか確認
git diff --check

複数ログソース特有の注意点

今回のようにNginx・Kubernetes・Docker・アプリケーションログ・GitHub Actionsなど異なる形式のログを1つのAPIで受け付ける設計では、ソースごとに専用のパーサーとバリデーションスキーマを用意し、共通の正規化フォーマット(タイムスタンプ、重大度、サービス名、メッセージなど)に変換する構成が有効です。

ソース固有の情報(NginxのHTTPステータスコード、KubernetesのNamespaceやPod情報など)はメタデータ欄に残しておくことで、後から元データを追跡できます。

バッチ投入エンドポイント(1〜100件まで受け付ける設計など)を用意する場合は、1件でも不正なレコードがあればリクエスト全体を拒否するか、部分的に受け付けるかの方針を事前に決めておくと、テストケースの設計もぶれません。

まとめ: テストの信頼性はDB分離から

ログ取り込みAPIのような、複数ソースを横断するバックエンドを開発する際は、手動のライブチェックと自動テストが同じDBを共有していないか、まず疑ってみてください。

確認の第一歩は、conftest.pyにテスト専用のDBオーバーライドとロールバック処理があるかどうかのチェックです。

なければ、専用DBの作成、Alembicでのマイグレーション適用、FastAPIの依存性注入オーバーライドの3点セットを順番に組み込むことで、テストの不安定さはかなり解消できます。

CI上でテストが「たまに失敗する」状態に心当たりがあるなら、今回の手順を一度試してみる価値はあるはずです。

参考

🛠️ Building Multi-Source Log Ingestion for an AI DevOps Incident Copilot

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。