本文 AI 產出,尚未審核

FastAPI 測試與除錯 – 模擬 Request / Response


簡介

在開發 API 時,測試 是保證功能正確與穩定的關鍵,而 模擬 (mock) request/response 則是單元測試與整合測試中最常用的技巧。
透過模擬,我們可以在不啟動真實伺服器的情況下,驗證路由、依賴注入、驗證器以及中介層 (middleware) 的行為,並快速定位程式錯誤。

FastAPI 本身提供了 TestClient(基於 Starlette 的測試客戶端)以及 Dependency Overriding 機制,使得模擬請求與回應變得相當直觀。本文將一步步說明如何在 FastAPI 專案中 模擬 request / response,並分享實務上常見的陷阱與最佳實踐,幫助你寫出更可靠的測試程式。


核心概念

1. 為什麼要模擬 Request / Response?

  • 獨立性:測試不依賴外部服務(資料庫、第三方 API),避免因環境差異導致測試不穩定。
  • 速度:不需要啟動實體伺服器或建立真實連線,測試執行時間可縮短至毫秒級。
  • 可預測性:透過固定的回傳資料,測試結果可預測,方便驗證邏輯正確性。

2. FastAPI 的測試工具

工具 目的 主要特性
TestClient 模擬 HTTP 請求 基於 requests,支援同步與非同步端點
override_dependency 替換依賴 可在測試期間注入 mock 物件或函式
AsyncClient (httpx) 非同步測試 與 FastAPI 完全相容的非同步 HTTP 客戶端

小技巧:在測試檔案中,通常會把 appclient 放在 conftest.py,讓 pytest 能自動載入。

3. 基本範例:使用 TestClient 模擬 GET 請求

# test_main.py
from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
    """簡單回傳查詢參數與路徑參數"""
    return {"item_id": item_id, "q": q}

client = TestClient(app)

def test_read_item():
    # 模擬 GET /items/42?q=hello
    response = client.get("/items/42?q=hello")
    assert response.status_code == 200
    assert response.json() == {"item_id": 42, "q": "hello"}

這段程式展示了最基本的 模擬請求,不需要啟動任何伺服器,只要呼叫 client.get() 即可取得回應物件。

4. Mock 依賴:取代資料庫存取

假設我們有一個依賴,用於從資料庫取得使用者資訊:

# dependencies.py
from typing import Protocol

class UserRepo(Protocol):
    async def get_user(self, user_id: int) -> dict:
        ...

# real_repo.py
class RealUserRepo:
    async def get_user(self, user_id: int) -> dict:
        # 真實的 DB 查詢
        ...

# main.py
from fastapi import Depends, FastAPI
from dependencies import UserRepo

app = FastAPI()

def get_user_repo() -> UserRepo:
    return RealUserRepo()

@app.get("/users/{user_id}")
async def read_user(user_id: int, repo: UserRepo = Depends(get_user_repo)):
    user = await repo.get_user(user_id)
    return user

在測試時,我們可以 覆寫 get_user_repo,提供一個 mock 物件:

# test_user.py
import pytest
from fastapi.testclient import TestClient
from main import app, get_user_repo

class MockUserRepo:
    async def get_user(self, user_id: int) -> dict:
        # 回傳固定資料,避免真的 DB 呼叫
        return {"id": user_id, "name": "測試使用者"}

# 覆寫依賴
app.dependency_overrides[get_user_repo] = lambda: MockUserRepo()
client = TestClient(app)

def test_read_user():
    response = client.get("/users/7")
    assert response.status_code == 200
    assert response.json() == {"id": 7, "name": "測試使用者"}

# 測試結束後清除覆寫,避免影響其他測試
@pytest.fixture(autouse=True)
def reset_overrides():
    yield
    app.dependency_overrides.clear()

透過 dependency_overrides,我們 不必真的連接資料庫,就能驗證路由的回傳格式與錯誤處理邏輯。

5. 模擬外部 API 呼叫

若 API 內部會向第三方服務發送 HTTP 請求,建議使用 httpx.MockTransportresponses 套件來模擬回應。

# external.py
import httpx

async def fetch_weather(city: str) -> dict:
    async with httpx.AsyncClient() as client:
        r = await client.get(f"https://api.weather.com/{city}")
        r.raise_for_status()
        return r.json()

測試時使用 httpx.MockTransport

# test_external.py
import pytest
import httpx
from external import fetch_weather

@pytest.mark.asyncio
async def test_fetch_weather():
    # 建立 mock 回傳資料
    async def mock_handler(request):
        assert request.url.path == "/Taipei"
        return httpx.Response(200, json={"temp": 28, "status": "晴"})

    transport = httpx.MockTransport(mock_handler)

    async with httpx.AsyncClient(transport=transport) as client:
        # 以相同方式呼叫 fetch_weather,只是內部使用 mock client
        result = await fetch_weather("Taipei")
        assert result == {"temp": 28, "status": "晴"}

重點MockTransport 只會攔截 httpx.AsyncClient 的請求,讓我們可以 完全控制外部 API 的回應,而不必真的對外發送請求。

6. 測試 Middleware 與例外處理

假設我們有一段自訂 Middleware,會在每個 response 加上 X-Request-ID

# middleware.py
import uuid
from starlette.middleware.base import BaseHTTPMiddleware

class RequestIDMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        request_id = str(uuid.uuid4())
        response = await call_next(request)
        response.headers["X-Request-ID"] = request_id
        return response

測試這段 Middleware:

# test_middleware.py
from fastapi import FastAPI
from fastapi.testclient import TestClient
from middleware import RequestIDMiddleware

app = FastAPI()
app.add_middleware(RequestIDMiddleware)

@app.get("/ping")
def ping():
    return {"msg": "pong"}

client = TestClient(app)

def test_request_id_header():
    resp = client.get("/ping")
    assert resp.status_code == 200
    # 確認 Header 被正確加入
    assert "X-Request-ID" in resp.headers
    # Header 必須是 UUID 格式
    import re
    uuid_pat = re.compile(r"^[0-9a-fA-F-]{36}$")
    assert uuid_pat.match(resp.headers["X-Request-ID"])

這裡我們直接使用 TestClient,而不需要額外的 mock;因為 Middleware 本身只在請求/回應的生命週期中插手,測試時即可觀察最終的 HTTP Header。


常見陷阱與最佳實踐

陷阱 說明 解決方式
依賴未正確覆寫 測試時仍然呼叫真實 DB,導致測試緩慢或失敗。 使用 app.dependency_overrides,並在測試結束後 clear()
同步 vs 非同步混用 TestClient 中呼叫非同步端點時忘記使用 await,會得到 RuntimeError 若測試非同步端點,使用 httpx.AsyncClientpytest-asyncio
全域狀態污染 測試中修改全域變數或單例,影響其他測試。 盡量把狀態封裝在依賴或函式內,使用 fixture 產生獨立實例。
Mock 回傳不符合實際結構 回傳的資料與真實 API 格式不一致,測試通過但上線失敗。 參考 OpenAPI schema 或使用 pydanticparse_obj 進行驗證。
忘記測試例外路徑 只測試成功情況,忽略了例外處理。 針對 4xx/5xx、timeout、validation error 等情況寫測試。

最佳實踐

  1. 分層測試:先對 Service 層、Repository 層寫單元測試(使用 mock),再對 API 路由寫整合測試(使用 TestClient)。
  2. 使用 Fixtures:將 clientappdependency_overrides 包裝成 pytest fixture,保證測試的隔離性可重用性
  3. 驗證回傳結構:結合 Pydantic model,使用 model.parse_obj(response.json()) 確保回傳符合預期 schema。
  4. 保持測試快:每個測試的執行時間建議不超過 200ms,過慢的測試往往是因為真的呼叫了外部資源。
  5. CI 整合:將測試指令加入 GitHub Actions、GitLab CI 等,確保每次 PR 都會跑完整測試。

實際應用場景

  1. 開發新功能時驗證資料流

    • 當新增一個需要同時呼叫外部信用評分 API 與內部 DB 的端點時,可先用 MockTransport 模擬信用評分回應,再用 dependency_overrides 模擬 DB,確認整體邏輯正確。
  2. 回歸測試

    • 專案升級 FastAPI 版號或改寫中介層時,執行完整的 TestClient 整合測試,確保舊有路由仍保持相同的 HTTP 狀態碼與回傳結構。
  3. 性能基準測試

    • 使用 TestClient 搭配 pytest-benchmark,在不啟動真實伺服器的情況下測量每個端點的平均回應時間,快速定位瓶頸。
  4. 安全測試

    • 透過 mock 產生惡意請求(如缺少 JWT、過長的 body),驗證 API 的驗證與錯誤回傳是否符合安全需求。

總結

模擬 request / response 是 FastAPI 測試流程中不可或缺的一環。透過 TestClientdependency_overrides、以及 httpx.MockTransport,我們可以:

  • 快速、可靠 地驗證路由、依賴與中介層的行為
  • 避免外部資源 造成測試不穩定或執行時間過長
  • 在 CI/CD 中自動化測試,提升程式碼品質與部署信心

掌握上述技巧後,你將能在 開發階段即捕捉錯誤,並在 上線前提供完整的測試覆蓋,讓 FastAPI 應用更具彈性與可維護性。祝你寫測試寫得順利,開發更快、更好!