本文 AI 產出,尚未審核

FastAPI 測試與除錯:pytest + TestClient 完全指南


簡介

FastAPI 開發專案時,測試是確保 API 正確性與穩定性的關鍵步驟。即使功能看起來運作正常,缺乏自動化測試仍可能在未來的需求變動或重構時埋下隱憂。利用 pytest 搭配 FastAPI 的 TestClient,我們可以在不啟動真實伺服器的情況下,直接對路由、請求與回應做完整驗證,讓除錯過程變得更快、更可靠。

本篇文章將從核心概念說明開始,透過 5 個實用範例 示範如何寫測試、模擬依賴、檢查例外與驗證資料模型,最後整理常見陷阱、最佳實踐與真實專案的應用情境,幫助初學者到中級開發者快速上手測試與除錯。


核心概念

1. 為什麼選擇 pytest?

  • 簡潔的語法:只要寫 assert,pytest 會自動產生清晰的錯誤訊息。
  • 強大的插件生態:如 pytest-asynciopytest-cov 可支援異步測試與覆蓋率報告。
  • 自動發現測試:只要檔名符合 test_*.py*_test.py,pytest 會自動載入。

小技巧:在 pytest.ini 中設定 addopts = -ra -q,可以讓執行結果更簡潔。

2. TestClient 的角色

TestClient 其實是 Starlette(FastAPI 底層框架)提供的測試用 HTTP 客戶端,內部使用 requests 介面模擬 HTTP 呼叫。它的好處在於:

  • 不需要啟動實體伺服器,測試速度快。
  • 支援完整的請求流程(headers、cookies、JSON body 等)。
  • 可以直接取得 FastAPI 的依賴注入容器,方便測試時覆寫依賴。

3. 基本測試結構

# tests/test_main.py
from fastapi.testclient import TestClient
from app.main import app   # 這裡的 app 為 FastAPI 實例

client = TestClient(app)

def test_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World"}

重點:測試檔案只需要匯入 app,不需要額外的 uvicorndocker 設定。


程式碼範例

以下示範 5 個常見且實務導向的測試情境,全部使用 pytest + TestClient

範例 1:測試 JSON 請求與回應

# tests/test_user.py
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_create_user():
    payload = {"username": "alice", "email": "alice@example.com"}
    response = client.post("/users/", json=payload)
    assert response.status_code == 201
    data = response.json()
    assert data["id"] is not None
    assert data["username"] == "alice"
    assert data["email"] == "alice@example.com"
  • 使用 json= 參數自動將字典序列化為 JSON。
  • response.json() 會把回傳的 JSON 直接轉成 Python 結構,方便斷言。

範例 2:測試帶有路徑參數的 GET

def test_get_user():
    # 先建立一筆測試資料
    create_resp = client.post("/users/", json={"username": "bob", "email": "bob@example.com"})
    user_id = create_resp.json()["id"]

    # 取得剛才建立的使用者
    resp = client.get(f"/users/{user_id}")
    assert resp.status_code == 200
    assert resp.json()["username"] == "bob"
  • 透過前置步驟建立測試資料,確保測試是 自足 的。

範例 3:模擬依賴(Dependency Override)

FastAPI 允許在測試時 override 依賴,以避免真的呼叫外部服務(如資料庫、第三方 API)。

# app/dependencies.py
def get_db():
    # 正式環境會回傳資料庫 Session
    ...

# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.dependencies import get_db

class FakeDB:
    def __init__(self):
        self.storage = {}

    def add_user(self, user):
        self.storage[user["id"]] = user
        return user

    def get_user(self, user_id):
        return self.storage.get(user_id)

@pytest.fixture
def client():
    fake_db = FakeDB()
    # 覆寫依賴
    app.dependency_overrides[get_db] = lambda: fake_db
    with TestClient(app) as c:
        yield c
    # 清除覆寫
    app.dependency_overrides.clear()
def test_user_via_fake_db(client):
    # 直接使用 client fixture
    resp = client.post("/users/", json={"username": "cathy", "email": "cathy@example.com"})
    assert resp.status_code == 201
    user_id = resp.json()["id"]
    # 透過覆寫的 FakeDB 取得資料
    assert client.app.dependency_overrides[get_db]().get_user(user_id)["username"] == "cathy"
  • 關鍵app.dependency_overrides 只在測試期間有效,測試結束後記得 clear()

範例 4:測試異步路由與 pytest-asyncio

若路由使用 async def,仍可使用同步的 TestClient,但若想直接測試非 HTTP 的協程函式,則需要 pytest-asyncio

# tests/test_async.py
import pytest
from httpx import AsyncClient
from app.main import app

@pytest.mark.asyncio
async def test_async_hello():
    async with AsyncClient(app=app, base_url="http://test") as ac:
        resp = await ac.get("/async-hello")
    assert resp.status_code == 200
    assert resp.json() == {"msg": "Hello from async endpoint"}
  • 說明httpx.AsyncClient 也是 FastAPI 官方推薦的非同步測試工具。

範例 5:驗證例外處理與錯誤回傳

def test_invalid_user_id():
    resp = client.get("/users/invalid-id")
    assert resp.status_code == 422  # FastAPI 會自動回傳驗證錯誤
    json_body = resp.json()
    assert json_body["detail"][0]["loc"] == ["path", "user_id"]
    assert "value is not a valid integer" in json_body["detail"][0]["msg"]
  • 透過測試確認 validation error自訂例外(如 HTTPException(status_code=404))的行為是否如預期。

常見陷阱與最佳實踐

陷阱 可能原因 解決方案
測試相依於真實資料庫 直接使用 SessionLocal() 而未覆寫 使用 dependency_overrides 注入 in‑memory SQLiteFakeDB
測試順序影響結果 前一個測試留下資料,後一個測試假設乾淨環境 每個測試使用 fixture 建立與銷毀獨立環境,或在 client fixture 中使用 transaction.rollback()
異步路由測試失敗 使用同步 TestClient 呼叫 async 函式時未等候 改用 httpx.AsyncClient 並加上 @pytest.mark.asyncio
錯誤訊息不明確 直接 assert response.text,忽略 JSON 結構 透過 response.json()["detail"] 取得結構化錯誤資訊,並斷言關鍵欄位。
測試覆寫未清除 app.dependency_overrides 在多個測試間共用 在 fixture 的 yieldapp.dependency_overrides.clear(),或使用 autouse=True 的 fixture 進行自動清理。

最佳實踐

  1. 測試即文件:每個路由的測試檔案名稱應與路由檔案對應,讓新同事能快速了解 API 行為。
  2. 使用 fixtures 統一管理 client:在 conftest.py 中提供 client fixture,所有測試共享同一套設定。
  3. 加入覆蓋率報告:執行 pytest --cov=app -vv,確保核心業務邏輯的覆蓋率達到 80% 以上。
  4. CI/CD 整合:將測試指令寫入 GitHub Actions 或 GitLab CI,確保每次 PR 都必須通過測試。
  5. 保持測試獨立:不要在測試中依賴全域變數或外部狀態,使用 factory-boyfaker 產生測試資料。

實際應用場景

1. 企業內部微服務 API

在微服務架構下,每個服務的 API 必須遵守嚴格的合約。透過 pytest + TestClient,開發團隊可以在 CI 階段自動驗證:

  • 請求與回應的 JSON Schema 是否符合 OpenAPI 定義。
  • 授權機制(JWT、OAuth2)是否正確拋出 401/403。

2. 第三方整合測試

當 FastAPI 需要呼叫外部支付或訊息服務時,使用 dependency override 注入 Mock 客戶端,即可在本地測試所有成功與失敗情境,而不必真的向第三方發送請求。

3. 演算法或資料處理服務

若 API 背後執行複雜的機器學習模型,測試可以:

  • 把模型載入改為 輕量的 stub,確保 API 邏輯(參數驗證、回傳格式)正確。
  • 使用 pytest.mark.parametrize 測試多組輸入與預期輸出,快速定位邊界值問題。

總結

  • pytest + TestClient 為 FastAPI 測試提供了 快速、簡潔且功能完整 的解決方案。
  • 透過 dependency overridesfixture異步測試支援,我們可以在不依賴真實外部資源的情況下,完整驗證路由、資料模型與例外處理。
  • 注意常見的測試陷阱(資料庫相依、測試順序、異步錯誤等),並遵循最佳實踐(測試即文件、CI 整合、覆蓋率監控),即可讓專案在持續開發與部署過程中保持高品質與可維護性。

最後提醒:寫測試不是為了追求 100% 的覆蓋率,而是為了在功能變更時,快速捕捉破壞性改動,讓除錯變得更有效率。祝你在 FastAPI 的測試旅程中順利前行!