週一早上,產品經理突然跑來:「客戶反映說找不到重要的待辦事項!他們有 300 多筆資料,全部擠在同一頁...」你打開測試環境一看,密密麻麻的待辦事項像瀑布一樣流下來。沒有分類、沒有篩選、沒有分頁,這不是待辦清單,這是待辦災難!
今天,我們要為 Todo API 加入篩選功能和路由,讓使用者能夠輕鬆管理大量的待辦事項。更重要的是,我們要用 TDD 的方式確保這些功能在各種情況下都能正常運作。
基礎測試 [##########] 100% ✅ (Day 1-10)
Roman Kata [#######] 100% ✅ (Day 11-17)
框架特色 [######----] 60% 🚀 (Day 18-27)
↑ 我們在這裡!Day 23
想像一下這些情況:
這些都是真實世界中常見的問題。透過 TDD,我們能在開發階段就預防這些問題。
首先,我們要定義篩選的需求:
# 建立 tests/day23/test_todo_filter.py
import pytest
from fastapi.testclient import TestClient
from src.todo.main import app
client = TestClient(app)
def test_shows_all_todos_by_default():
"""測試預設顯示所有待辦事項"""
# 建立測試資料
todos = [
{"title": "Learn Python", "completed": False},
{"title": "Write Tests", "completed": True},
{"title": "Deploy App", "completed": False}
]
response = client.get("/api/todos")
assert response.status_code == 200
data = response.json()
assert len(data["todos"]) == 3
def test_filters_active_todos():
"""測試篩選進行中的待辦事項"""
response = client.get("/api/todos?filter=active")
assert response.status_code == 200
data = response.json()
# 確認只有未完成的項目
for todo in data["todos"]:
assert todo["completed"] is False
def test_filters_completed_todos():
"""測試篩選已完成的待辦事項"""
response = client.get("/api/todos?filter=completed")
assert response.status_code == 200
data = response.json()
# 確認只有已完成的項目
for todo in data["todos"]:
assert todo["completed"] is True
def test_returns_filter_counts():
"""測試返回各篩選狀態的數量"""
response = client.get("/api/todos")
assert response.status_code == 200
data = response.json()
assert "count" in data
assert "all" in data["count"]
assert "active" in data["count"]
assert "completed" in data["count"]
# 建立 src/todo/main.py
from fastapi import FastAPI, Query, Depends
from typing import Optional, List
from pydantic import BaseModel
app = FastAPI()
class Todo(BaseModel):
id: int
title: str
completed: bool
class TodoResponse(BaseModel):
todos: List[Todo]
count: dict
# 模擬資料庫
todos_db = [
Todo(id=1, title="Learn Python", completed=False),
Todo(id=2, title="Write Tests", completed=True),
Todo(id=3, title="Deploy App", completed=False)
]
@app.get("/api/todos", response_model=TodoResponse)
async def get_todos(
filter: Optional[str] = Query(None, regex="^(all|active|completed)$")
):
"""獲取待辦事項列表,支援篩選"""
if filter == "active":
filtered = [t for t in todos_db if not t.completed]
elif filter == "completed":
filtered = [t for t in todos_db if t.completed]
else:
filtered = todos_db
# 計算各狀態數量
counts = {
"all": len(todos_db),
"active": len([t for t in todos_db if not t.completed]),
"completed": len([t for t in todos_db if t.completed])
}
return TodoResponse(todos=filtered, count=counts)
@app.get("/api/todos/active")
async def get_active_todos():
"""獲取進行中的待辦事項"""
return await get_todos(filter="active")
@app.get("/api/todos/completed")
async def get_completed_todos():
"""獲取已完成的待辦事項"""
return await get_todos(filter="completed")
現在讓我們測試篩選功能如何透過路由與 API 整合:
import pytest
from fastapi.testclient import TestClient
from src.todo.main import app
client = TestClient(app)
def test_loads_all_todos_at_root_path():
"""測試根路徑載入所有待辦事項"""
response = client.get("/api/todos")
assert response.status_code == 200
data = response.json()
assert len(data["todos"]) == 3
def test_loads_active_filter_from_url():
"""測試從 URL 載入進行中的篩選"""
response = client.get("/api/todos/active")
assert response.status_code == 200
data = response.json()
# 確認只有未完成的項目
for todo in data["todos"]:
assert todo["completed"] is False
def test_loads_completed_filter_from_url():
"""測試從 URL 載入已完成的篩選"""
response = client.get("/api/todos/completed")
assert response.status_code == 200
data = response.json()
# 確認只有已完成的項目
for todo in data["todos"]:
assert todo["completed"] is True
def test_preserves_filter_on_refresh():
"""測試重新整理後保持篩選狀態"""
# 第一次請求
response1 = client.get("/api/todos/active")
data1 = response1.json()
# 第二次請求(模擬重新整理)
response2 = client.get("/api/todos/active")
data2 = response2.json()
assert data1 == data2
好的測試要考慮各種邊界情況:
import pytest
from fastapi.testclient import TestClient
from src.todo.main import app, todos_db
client = TestClient(app)
def test_handles_empty_todo_list():
"""測試空的待辦清單"""
# 暫時清空資料
original = todos_db.copy()
todos_db.clear()
response = client.get("/api/todos")
assert response.status_code == 200
data = response.json()
assert len(data["todos"]) == 0
assert data["count"]["all"] == 0
assert data["count"]["active"] == 0
assert data["count"]["completed"] == 0
# 恢復資料
todos_db.extend(original)
def test_handles_all_completed_todos():
"""測試所有項目都已完成"""
# 修改所有項目為已完成
for todo in todos_db:
todo.completed = True
response = client.get("/api/todos/active")
data = response.json()
assert len(data["todos"]) == 0
response = client.get("/api/todos/completed")
data = response.json()
assert len(data["todos"]) == len(todos_db)
def test_handles_all_active_todos():
"""測試所有項目都未完成"""
# 修改所有項目為未完成
for todo in todos_db:
todo.completed = False
response = client.get("/api/todos/active")
data = response.json()
assert len(data["todos"]) == len(todos_db)
response = client.get("/api/todos/completed")
data = response.json()
assert len(data["todos"]) == 0
當待辦事項數量很多時,效能變得很重要。透過查詢優化和快取,我們可以提升 API 回應速度。
import pytest
import time
from fastapi.testclient import TestClient
from src.todo.main import app, Todo, todos_db
client = TestClient(app)
def test_efficiently_handles_large_datasets():
"""測試處理大量資料的效能"""
# 建立大量測試資料
original = todos_db.copy()
todos_db.clear()
for i in range(100):
todos_db.append(
Todo(id=i+1, title=f"Task {i+1}", completed=i % 2 == 0)
)
start = time.time()
response = client.get("/api/todos")
duration = time.time() - start
assert response.status_code == 200
assert duration < 0.5 # 應在 500ms 內完成
# 恢復原始資料
todos_db.clear()
todos_db.extend(original)
def test_filter_performance():
"""測試篩選效能"""
# 建立大量測試資料
original = todos_db.copy()
todos_db.clear()
for i in range(100):
todos_db.append(
Todo(id=i+1, title=f"Task {i+1}", completed=i % 2 == 0)
)
start = time.time()
# 執行多個篩選請求
client.get("/api/todos?filter=all")
client.get("/api/todos?filter=active")
client.get("/api/todos?filter=completed")
duration = time.time() - start
assert duration < 1.0 # 三個請求應在 1 秒內完成
# 恢復原始資料
todos_db.clear()
todos_db.extend(original)
更新 src/todo/main.py 加入快取機制
from fastapi import FastAPI, Query, Depends
from typing import Optional, List, Dict
from pydantic import BaseModel
from functools import lru_cache
import time
app = FastAPI()
class Todo(BaseModel):
id: int
title: str
completed: bool
# 簡單的記憶體快取
cache: Dict[str, tuple] = {}
CACHE_TTL = 60 # 快取 60 秒
def get_cached_or_compute(key: str, compute_func):
"""獲取快取或計算結果"""
if key in cache:
data, timestamp = cache[key]
if time.time() - timestamp < CACHE_TTL:
return data
data = compute_func()
cache[key] = (data, time.time())
return data
@app.get("/api/todos")
async def get_todos_optimized(
filter: Optional[str] = Query(None, regex="^(all|active|completed)$")
):
"""優化版的待辦事項 API"""
cache_key = f"todos_{filter or 'all'}"
def compute():
if filter == "active":
filtered = [t for t in todos_db if not t.completed]
elif filter == "completed":
filtered = [t for t in todos_db if t.completed]
else:
filtered = todos_db
counts = {
"all": len(todos_db),
"active": len([t for t in todos_db if not t.completed]),
"completed": len([t for t in todos_db if t.completed])
}
return {"todos": filtered, "count": counts}
return get_cached_or_compute(cache_key, compute)
試著為你的篩選功能加入這些測試:
今天我們學到了:
✅ 如何用 TDD 開發 API 篩選功能
✅ 測試 URL 路由與篩選狀態
✅ 處理篩選的邊界情況
✅ 效能優化的測試策略
✅ 使用快取提升 API 效能
這些測試技巧不只適用於待辦事項,任何需要資料篩選的 API 都能使用。記住,好的篩選功能測試要考慮:
明天(Day 24)我們將探討「測試檔案上傳功能」,學習如何處理檔案上傳的各種情境!
記住:篩選功能看似簡單,但魔鬼藏在細節裡。透過完整的測試,我們能確保 API 的穩定性和效能!