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.mock 的 MagicMock、patch 能提供更細緻的控制。
範例 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_effect讓mock_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。 |
最佳實踐
- 以
dependency_overrides為主:保持測試程式碼與應用程式碼分離。 - 使用
Protocol或抽象基底類別:讓 Mock 能夠在型別檢查時保證相容性。 - 把 Mock 造型集中管理:建立
tests/conftest.py,在裡面寫好常用的 Mock factory,提升可讀性與維護性。 - 測試正向與負向情境:不只測試成功流程,也要測試例外、驗證失敗、權限不足等情況。
- 結合 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。
- 實作:
- 用
patch把ldap3.Connection替換成MockConnection,設定bind成功或失敗。 jwt.encode也可以用MagicMock產生固定的 token,方便斷言。
- 用
3. 微服務間的訊息佇列 (RabbitMQ / Kafka)
- 依賴:
aio-pika或aiokafka的 Producer。 - 測試需求:
- 確認在特定事件發生時,訊息會被正確送出。
- 若佇列不可用,服務應回傳錯誤且不寫入資料庫。
- 實作:
- 建立
AsyncMockProducer,實作publish為 async 方法,記錄呼叫參數。 - 在測試中
app.dependency_overrides[get_producer] = lambda: AsyncMockProducer(),配合await測試非同步流程。
- 建立
總結
依賴注入是 FastAPI 強大的設計基礎,而 Mock 則是讓這套機制在測試與除錯時發揮最大效益的關鍵工具。透過 dependency_overrides、unittest.mock(MagicMock、AsyncMock、patch)以及 分層、聚焦 的測試策略,我們可以:
- 快速、可靠 地驗證業務邏輯。
- 模擬各種成功與失敗情境,提升錯誤處理的完整度。
- 保持測試與程式碼的低耦合,讓未來的重構與功能擴充更為順暢。
記得在實作時遵循「只 Mock 外部資源、保留內部邏輯」的原則,並在測試結束後清除 dependency_overrides,以免影響其他測試。結合 CI、型別檢查與良好的測試結構,你的 FastAPI 專案將會在 品質、維護性與開發速度 上得到全方位的提升。祝你寫測試寫得開心、除錯寫得順利! 🚀