FastAPIテスト完全ガイド
FastAPIテスト完全ガイド
Section titled “FastAPIテスト完全ガイド”FastAPIアプリケーションのテストを、実務で使える実装例とともに詳しく解説します。
1. テストとは
Section titled “1. テストとは”テストの重要性
Section titled “テストの重要性”テストは、アプリケーションの品質を保証し、リファクタリングを安全に行うために不可欠です。
テストの種類 ├─ 単体テスト(Unit Test) ├─ 統合テスト(Integration Test) └─ E2Eテスト(End-to-End Test)なぜテストが必要か
Section titled “なぜテストが必要か”問題のある構成(テストなし):
# 問題: 手動での動作確認@app.get("/users/{user_id}")def get_user(user_id: int): # 手動で動作確認が必要 return {"user_id": user_id}解決: 自動テストによる品質保証
# 解決: 自動テストdef test_get_user(): response = client.get("/users/1") assert response.status_code == 200 assert response.json() == {"user_id": 1}FastAPIのテスト方法ガイド 🧪
Section titled “FastAPIのテスト方法ガイド 🧪”FastAPIでは、TestClient を使用してAPIエンドポイントを直接テストするのが最も一般的です。TestClientは、実際のネットワーク通信をせずに仮想的なリクエストをアプリケーションに送信するため、高速かつ信頼性の高いテストが可能です。
1. 必要なライブラリのインストール ⚙️
Section titled “1. 必要なライブラリのインストール ⚙️”まず、テストに必要なpytestとhttpxをインストールします。
pip install "pytest" "httpx[standard]"2. 基本的なテストコードの作成 (結合テスト) 🤝
Section titled “2. 基本的なテストコードの作成 (結合テスト) 🤝”TestClientを使って、アプリケーション全体が正しく連携して動くかをテストします。
フォルダ構成:
Section titled “フォルダ構成:”.├── app/│ └── main.py└── tests/ └── test_main.pytests/test_main.pyのコード例:
Section titled “tests/test_main.pyのコード例:”from fastapi.testclient import TestClientfrom app.main import app
client = TestClient(app)
def test_read_users(): """GET /users/ のエンドポイントをテスト""" response = client.get("/users/") assert response.status_code == 200 assert response.json() == [ {"id": 1, "name": "Alice", "email": "alice@example.com"}, {"id": 2, "name": "Bob", "email": "bob@example.com"}, ]
def test_create_user(): """POST /users/ のエンドポイントをテスト""" new_user_data = {"name": "Charlie", "email": "charlie@example.com"} response = client.post("/users/", json=new_user_data) assert response.status_code == 201 assert response.json() == {"id": 3, "name": "Charlie", "email": "charlie@example.com"}TestClientは、実際のHTTPリクエストと同様に.get()や.post()などのメソッドを使えるため、直感的にテストが記述できます。
3. より実践的なテスト手法 🚀
Section titled “3. より実践的なテスト手法 🚀”a. データベースを使用する場合のテスト (フィクスチャ)
Section titled “a. データベースを使用する場合のテスト (フィクスチャ)”データベースに依存するテストでは、各テストが独立して実行されるように、テストの前後にデータベースを初期化・クリーンアップする処理が不可欠です。pytestのフィクスチャを使うと、この処理を自動化できます。
conftest.pyのコード例:
Section titled “conftest.pyのコード例:”# conftest.py (テストのルートディレクトリに配置)
import pytestfrom sqlalchemy import create_enginefrom sqlalchemy.orm import sessionmakerfrom app.database import Basefrom app.main import app
@pytest.fixture(scope="function")def client(): # 依存性を上書きし、テスト用DBに接続 def override_get_db(): # ... テスト用DBのセッションを返すロジック ... pass
app.dependency_overrides[get_db] = override_get_db with TestClient(app) as client: yield client app.dependency_overrides.clear() # 依存性のオーバーライドを解除このフィクスチャを配置することで、テストごとにクリーンな状態のテストデータベースが利用できるようになります。
b. モック(Mocking)を使ったテスト (単体テスト) 🎯
Section titled “b. モック(Mocking)を使ったテスト (単体テスト) 🎯”モックとは、外部サービス(データベース、外部APIなど)を**偽物(モックオブジェクト)**に置き換える手法です。これにより、外部に依存しない高速な単体テストが書けます。
pytestでは、monkeypatchフィクスチャを使って簡単にモックを実現できます。
tests/test_main_mock.pyのコード例:
Section titled “tests/test_main_mock.pyのコード例:”from fastapi.testclient import TestClientfrom app.main import appfrom app.schemas.user import Userfrom app.services import db_service
client = TestClient(app)
def test_read_users_mocked(monkeypatch): """get_users関数をモックしてテスト""" mock_users = [User(id=1, name="MockUser", email="mock@example.com")]
def mock_get_users(): return mock_users
monkeypatch.setattr(db_service, "get_users", mock_get_users)
response = client.get("/users/") assert response.status_code == 200 assert response.json() == [{"id": 1, "name": "MockUser", "email": "mock@example.com"}]このテストでは、db_service.pyが実際のデータベースに接続するかどうかに関わらず、エンドポイントの振る舞いだけを検証できます。
4. テストの実行 🚀
Section titled “4. テストの実行 🚀”プロジェクトのルートディレクトリで以下のコマンドを実行するだけで、pytestがテストを自動で実行してくれます。
pytestこれにより、アプリケーションの品質と信頼性を確保し、安心して開発を進めることができます。
テストカバレッジの測定 📊
Section titled “テストカバレッジの測定 📊”テストコードがアプリケーションのどの部分をどれだけカバーしているかを把握することは、品質を確保する上で非常に重要です。テストカバレッジを測定することで、テストが不足している箇所(カバレッジホール)を特定し、効率的にテストを追加できます。
coverage.py を使用すると、この測定を簡単に行えます。
インストール
Section titled “インストール”coverage.pyをインストールします。
pip install coverageカバレッジ付きでテストを実行
Section titled “カバレッジ付きでテストを実行”pytestを直接実行する代わりに、coverageコマンドを介して実行します。
coverage run -m pytestレポートの生成
Section titled “レポートの生成”カバレッジ情報を読みやすいレポート形式で表示します。
coverage reportこのコマンドは、各ファイルのカバレッジ率(行数ベース)を一覧で表示します。
より詳細なレポートが必要な場合は、HTML形式で生成することも可能です。
coverage htmlこれにより、htmlcov/ディレクトリに詳細なレポートが生成されます。ファイルを開くと、コードの各行がテストされたかどうかを色分けで確認できます。
その他のテストに関するヒント 💡
Section titled “その他のテストに関するヒント 💡”- 命名規則の統一: テスト関数の名前は
test_で始め、何のためにテストしているのかを明確に記述しましょう(例:test_create_user_success)。 - テストの分離: 各テストは完全に独立しているべきです。テスト間で状態が共有されないように、可能な限りフィクスチャを活用しましょう。
- 継続的インテグレーション(CI): GitHub ActionsなどのCIツールと連携し、コードがプッシュされるたびに自動でテストが実行されるように設定することで、品質管理を自動化できます。
責務別のテストの考え方 🧪
Section titled “責務別のテストの考え方 🧪”Model (ロジック) のテスト: 単体テスト 🎯
Section titled “Model (ロジック) のテスト: 単体テスト 🎯”Model層は、アプリケーションのビジネスロジックやデータ操作を担う部分です。この層は、外部サービス(データベースなど)や他の層(View)から独立してテストすべきです。これを**単体テスト(Unit Test)**と呼びます。
- 目的: Modelの関数やクラスが、与えられた入力に対して常に期待通りの出力を返すか検証する。
- テスト対象: データベースの操作ロジック、データ変換、バリデーションなど。
- 利点:
- 高速: 外部サービスに接続しないため、テストが非常に高速です。
- 再現性: 外部の状態に依存しないため、テストの失敗・成功が常に同じになります。
- 問題の特定: どこでバグが発生したかをピンポイントで特定できます。
- 実装方法:
pytestの**モック(Mocking)**機能を使って、データベース接続や外部APIコールを偽物に置き換えます。
View (エンドポイント) のテスト: 結合テスト 🤝
Section titled “View (エンドポイント) のテスト: 結合テスト 🤝”View層は、ユーザーからのリクエストを受け付け、適切なModel層のロジックを呼び出し、レスポンスを返す役割を担います。この層のテストは、**結合テスト(Integration Test)**として扱います。
- 目的: エンドポイントがHTTPリクエストに対して正しく応答するか(例: 正しいステータスコード、正しいJSONレスポンス)、そしてModel層と適切に連携しているか検証する。
- テスト対象:
- HTTPメソッド(GET, POST, PUT, DELETE)
- URLパスとクエリパラメータ
- リクエストボディのバリデーション
- レスポンスの形式と内容
- 利点:
- システム全体の検証: アプリケーションの各コンポーネントが正しく連携していることを確認できます。
- エンドツーエンドの確認: ユーザーの視点からAPIが期待通りに動くことを保証できます。
- 実装方法: FastAPIのTestClientを使って、アプリケーション全体に仮想的なリクエストを送信します。
なぜテストを切り分けるべきか?
Section titled “なぜテストを切り分けるべきか?”テストを切り分けることで、以下のようなメリットがあります。
- 効率的なデバッグ: 結合テストが失敗した場合でも、単体テストが通っていれば、問題はModel層ではなくView層やその間の連携にあると絞り込めます。
- 高速なフィードバック: 単体テストは非常に高速なため、開発中に頻繁に実行し、早期にバグを発見できます。結合テストは単体テストよりも時間がかかるため、CI/CDパイプラインなどで実行します。
- メンテナンス性: Modelのロジックを変更しても、Viewのテストは壊れにくく、Viewのパスを変更してもModelのテストには影響しません。これにより、コードの変更に強いテストスイートが構築できます。
結論として、Model層はモックを使った単体テストでロジックの正確性を保証し、View層はTestClientを使った結合テストでシステム全体の振る舞いを検証する、というアプローチが理想的です。