本文 AI 產出,尚未審核

FastAPI 測試與除錯:依賴注入 (Dependency Injection) Mock 完全攻略


簡介

FastAPI 中,依賴注入 (Dependency Injection, DI) 是設計乾淨、可維護 API 的核心機制。它讓我們可以把資料庫連線、驗證服務、外部 API 等「副作用」抽離出路由函式,提升測試的可控性與可預測性。

然而,當我們要為這些依賴寫單元測試或整合測試時,直接呼叫真實資源往往不切實際:資料庫可能尚未建置、第三方服務可能收費或不穩定、甚至測試環境的網路會被防火牆阻擋。這時候 Mock(模擬)依賴就顯得極為重要。透過 Mock,我們可以:

  • 快速、可靠 地執行測試,避免外部因素干擾。
  • 驗證業務邏輯 是否正確,而不必關心底層資源的實作細節。
  • 在除錯時 只聚焦於程式本身,減少不相關的噪音。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握在 FastAPI 中如何使用依賴注入搭配 Mock 進行測試與除錯。


核心概念

1. 依賴注入的基本原理

FastAPI 透過 Depends 讓路由函式的參數自動注入相依物件。以下是一個最簡單的例子:

from fastapi import FastAPI, Depends

app = FastAPI()

def get_current_user():
    # 假設從 JWT 取得使用者資訊
    return {"username": "alice"}

@app.get("/me")
def read_me(user: dict = Depends(get_current_user)):
    return {"user": user}

get_current_user 為依賴函式,FastAPI 會在每次請求時呼叫它,然後把回傳值注入 read_me


2. 為何要 Mock 依賴?

  • 避免副作用:測試不該改動真實資料庫或發送真實郵件。
  • 提升執行速度:Mock 只在記憶體中運作,比起遠端呼叫快上數十倍。
  • 可預測的回傳:測試可以自行決定依賴回傳什麼,讓斷言更精確。

3. 使用 TestClient + override_dependency

FastAPI 內建 app.dependency_overrides 讓我們在測試時 替換 任意依賴。配合 TestClient,即可在不改動原始程式碼的前提下執行測試。

範例 1:Mock 取得使用者的依賴

from fastapi import FastAPI, Depends
from fastapi.testclient import TestClient

app = FastAPI()

def get_current_user():
    # 真實環境會從 JWT 解碼
    raise NotImplementedError

@app.get("/profile")
def profile(user: dict = Depends(get_current_user)):
    return {"username": user["username"]}

# ---------- 測試 ----------
def mock_get_current_user():
    # 這裡直接回傳測試用的使用者資料
    return {"username": "test_user"}

app.dependency_overrides[get_current_user] = mock_get_current_user
client = TestClient(app)

def test_profile():
    response = client.get("/profile")
    assert response.status_code == 200
    assert response.json() == {"username": "test_user"}

重點:只要在測試開始前把 app.dependency_overrides 設定好,所有對 /profile 的請求都會使用 mock_get_current_user,而不會觸發真正的 JWT 解碼。


4. 使用 unittest.mock 進一步控制行為

有時候我們需要 驗證依賴函式是否被正確呼叫,或是模擬例外情況。unittest.mockMagicMockpatch 能提供更細緻的控制。

範例 2:模擬資料庫存取(使用 patch

from fastapi import FastAPI, Depends, HTTPException
from fastapi.testclient import TestClient
from unittest.mock import patch

app = FastAPI()

def get_user_repo():
    # 真實環境會返回 ORM Session 或 Repository 物件
    ...

@app.get("/users/{user_id}")
def get_user(user_id: int, repo = Depends(get_user_repo)):
    user = repo.get(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

# ---------- 測試 ----------
def test_get_user_success():
    mock_repo = MagicMock()
    mock_repo.get.return_value = {"id": 1, "name": "Alice"}

    with patch("__main__.get_user_repo", return_value=mock_repo):
        client = TestClient(app)
        response = client.get("/users/1")
        assert response.status_code == 200
        assert response.json() == {"id": 1, "name": "Alice"}
        # 確保 repo.get 被呼叫一次且參數正確
        mock_repo.get.assert_called_once_with(1)

def test_get_user_not_found():
    mock_repo = MagicMock()
    mock_repo.get.return_value = None

    with patch("__main__.get_user_repo", return_value=mock_repo):
        client = TestClient(app)
        response = client.get("/users/999")
        assert response.status_code == 404
        assert response.json()["detail"] == "User not found"
  • patch("__main__.get_user_repo", …)整個依賴函式 取代為回傳我們自訂的 mock_repo
  • MagicMock 讓我們檢查 呼叫次數、參數,同時可自行設定回傳值或拋出例外。

5. 多層依賴的 Mock

在大型專案中,依賴往往是 巢狀 的:A 依賴 B,B 再依賴 C。只要把最底層的依賴換成 Mock,整條鏈都會受到影響。

範例 3:多層依賴的覆寫

from fastapi import FastAPI, Depends

app = FastAPI()

# 最底層:外部 API 客戶端
class PaymentGateway:
    def charge(self, amount: float):
        # 真實情況會呼叫第三方支付平台
        ...

def get_payment_gateway():
    return PaymentGateway()

# 中間層:商務服務
class OrderService:
    def __init__(self, gateway: PaymentGateway):
        self.gateway = gateway

    def create_order(self, amount: float):
        self.gateway.charge(amount)
        return {"status": "paid", "amount": amount}

def get_order_service(gateway: PaymentGateway = Depends(get_payment_gateway)):
    return OrderService(gateway)

@app.post("/order")
def place_order(amount: float, service: OrderService = Depends(get_order_service)):
    return service.create_order(amount)

# ---------- 測試 ----------
class MockGateway:
    def __init__(self):
        self.charged_amount = None

    def charge(self, amount: float):
        self.charged_amount = amount  # 只記錄,沒有真正付款

def test_place_order():
    mock_gateway = MockGateway()
    # 只 Override 最底層的依賴
    app.dependency_overrides[get_payment_gateway] = lambda: mock_gateway

    client = TestClient(app)
    response = client.post("/order", json={"amount": 123.45})
    assert response.status_code == 200
    assert response.json() == {"status": "paid", "amount": 123.45}
    # 確認 MockGateway 的 charge 被正確呼叫
    assert mock_gateway.charged_amount == 123.45

技巧:只 覆寫最底層 的依賴即可讓上層服務自動使用 Mock,維持測試的簡潔性。


6. 針對例外情況的 Mock

測試錯誤處理(Exception handling)時,我們往往想要 強迫依賴拋出例外,觀察 API 是否回傳正確的錯誤碼與訊息。

範例 4:模擬資料庫連線失敗

from fastapi import FastAPI, Depends, HTTPException
from fastapi.testclient import TestClient
from unittest.mock import MagicMock

app = FastAPI()

def get_db():
    # 真實環境會返回 Session
    ...

@app.get("/items")
def list_items(db = Depends(get_db)):
    try:
        return db.query_all()
    except Exception as exc:
        raise HTTPException(status_code=503, detail="Database unavailable") from exc

# ---------- 測試 ----------
def test_db_unavailable():
    mock_db = MagicMock()
    mock_db.query_all.side_effect = RuntimeError("connection lost")

    app.dependency_overrides[get_db] = lambda: mock_db
    client = TestClient(app)

    resp = client.get("/items")
    assert resp.status_code == 503
    assert resp.json()["detail"] == "Database unavailable"
  • side_effectmock_db.query_all 在被呼叫時拋出指定例外。
  • API 捕獲後轉換成 HTTPException,測試即可驗證錯誤碼與訊息。

常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記清除 dependency_overrides 測試後未還原會影響後續測試,導致不易偵測的錯誤。 pytest.fixture 中使用 yield,或在每個測試結束後 app.dependency_overrides.clear()
Mock 的行為與真實實作不一致 若 Mock 回傳的資料結構與實際物件不同,測試通過但程式在正式環境崩潰。 盡量 使用相同的類別或協定 (Protocol),或在 Mock 時使用 spec= 參數限制屬性。
過度 Mock 把所有依賴都換成 Mock,失去測試整合性的意義。 只 Mock 外部資源(DB、第三方 API),內部純粹的業務邏輯仍使用真實實作。
多執行緒/非同步測試時的競爭 TestClient 預設同步,若路由使用 async 仍可正常,但若使用背景任務或 WebSocket,需注意共享狀態。 為每個測試建立 獨立的 FastAPI app 實例,或使用 httpx.AsyncClient 配合 asyncio
忘記 await 非同步依賴 在 Mock 非同步函式時直接回傳值會造成 await 錯誤。 使用 AsyncMock(Python 3.8+)或自行建立 async def 的 mock。

最佳實踐

  1. dependency_overrides 為主:保持測試程式碼與應用程式碼分離。
  2. 使用 Protocol 或抽象基底類別:讓 Mock 能夠在型別檢查時保證相容性。
  3. 把 Mock 造型集中管理:建立 tests/conftest.py,在裡面寫好常用的 Mock factory,提升可讀性與維護性。
  4. 測試正向與負向情境:不只測試成功流程,也要測試例外、驗證失敗、權限不足等情況。
  5. 結合 CI:在 GitHub Actions 或 GitLab CI 中執行測試,確保每次提交都不會因依賴變更而破壞功能。

實際應用場景

1. 電子商務平台的訂單流程

  • 依賴:付款閘道 (第三方 API)、庫存服務、通知服務 (Email / SMS)。
  • 測試需求
    • 確認在付款成功時,訂單狀態會變為 paid
    • 若付款失敗,系統應回滾庫存、發送失敗通知。
  • 實作
    • 使用 dependency_overrides 把付款閘道換成 MockGateway,自行控制 charge 成功或拋出例外。
    • 庫存服務使用 MagicMock,驗證 reserve / release 是否被正確呼叫。

2. 內部管理系統的 LDAP 身分驗證

  • 依賴:LDAP 客戶端、JWT 產生器。
  • 測試需求
    • 測試不同使用者角色 (admin / normal) 是否能正確取得權限。
    • 模擬 LDAP 連線逾時,確認 API 回傳 503 Service Unavailable
  • 實作
    • patchldap3.Connection 替換成 MockConnection,設定 bind 成功或失敗。
    • jwt.encode 也可以用 MagicMock 產生固定的 token,方便斷言。

3. 微服務間的訊息佇列 (RabbitMQ / Kafka)

  • 依賴aio-pikaaiokafka 的 Producer。
  • 測試需求
    • 確認在特定事件發生時,訊息會被正確送出。
    • 若佇列不可用,服務應回傳錯誤且不寫入資料庫。
  • 實作
    • 建立 AsyncMockProducer,實作 publish 為 async 方法,記錄呼叫參數。
    • 在測試中 app.dependency_overrides[get_producer] = lambda: AsyncMockProducer(),配合 await 測試非同步流程。

總結

依賴注入是 FastAPI 強大的設計基礎,而 Mock 則是讓這套機制在測試與除錯時發揮最大效益的關鍵工具。透過 dependency_overridesunittest.mockMagicMockAsyncMockpatch)以及 分層、聚焦 的測試策略,我們可以:

  • 快速、可靠 地驗證業務邏輯。
  • 模擬各種成功與失敗情境,提升錯誤處理的完整度。
  • 保持測試與程式碼的低耦合,讓未來的重構與功能擴充更為順暢。

記得在實作時遵循「只 Mock 外部資源、保留內部邏輯」的原則,並在測試結束後清除 dependency_overrides,以免影響其他測試。結合 CI、型別檢查與良好的測試結構,你的 FastAPI 專案將會在 品質、維護性與開發速度 上得到全方位的提升。祝你寫測試寫得開心、除錯寫得順利! 🚀