昨天我們完成了 Roman Numeral Kata,現在進入框架特定測試的新階段!想像一個場景:專案上線前夕,PM 緊張地問:「API 都測試過了嗎?」你自信地回答:「每個端點都有完整的測試覆蓋!」這就是今天要學習的 HTTP 測試。
基礎階段 Kata 階段 框架特定測試
Days 1-10 Days 11-17 Days 18-27
✅ ✅ 📍 Day 18
[HTTP 測試基礎] <- 今天在這
⬇️
下一階段:更多測試技巧
在 Python 中,HTTP 測試允許我們:
面向 | 單元測試 | HTTP 測試 |
---|---|---|
測試範圍 | 單一函數或類別 | 完整請求流程 |
執行速度 | 極快 | 快 |
測試重點 | 邏輯正確性 | 整合正確性 |
覆蓋範圍 | 細節實作 | 使用者體驗 |
# 安裝 FastAPI 和測試工具
pip install fastapi httpx pytest-asyncio
# 建立 tests/day18/test_http_basics.py
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/api/health")
def health_check():
return {"status": "ok"}
client = TestClient(app)
def test_can_make_successful_get_request():
response = client.get("/api/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
# 建立 tests/day18/test_http_methods.py
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
email: str
users_db = {}
@app.get("/api/users")
def get_users():
return list(users_db.values())
@app.post("/api/users", status_code=201)
def create_user(user: User):
user_dict = user.model_dump()
user_dict["id"] = len(users_db) + 1
users_db[user_dict["id"]] = user_dict
return user_dict
@app.delete("/api/users/{user_id}", status_code=204)
def delete_user(user_id: int):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
del users_db[user_id]
client = TestClient(app)
def test_supports_different_http_methods():
# POST 請求
response = client.post("/api/users", json={
"name": "John Doe",
"email": "john@example.com"
})
assert response.status_code == 201
user_id = response.json()["id"]
# GET 請求
response = client.get("/api/users")
assert response.status_code == 200
assert len(response.json()) == 1
# DELETE 請求
response = client.delete(f"/api/users/{user_id}")
assert response.status_code == 204
# 建立 tests/day18/test_json_validation.py
def test_can_send_and_receive_json_data():
response = client.post("/api/users", json={
"name": "John Doe",
"email": "john@example.com",
"age": 30
})
assert response.status_code == 201
data = response.json()
# 驗證回應結構
assert "id" in data
assert data["name"] == "John Doe"
assert data["email"] == "john@example.com"
assert data["age"] == 30
# 驗證資料類型
assert isinstance(data["id"], int)
assert isinstance(data["name"], str)
# 建立 tests/day18/test_task_api.py
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class TaskCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=255)
completed: bool = False
tasks_db = {}
@app.get("/api/tasks")
def get_tasks():
return {"data": list(tasks_db.values())}
@app.post("/api/tasks", status_code=201)
def create_task(task: TaskCreate):
task_id = len(tasks_db) + 1
new_task = {
"id": task_id,
"title": task.title,
"completed": task.completed
}
tasks_db[task_id] = new_task
return {"data": new_task}
@app.get("/api/tasks/{task_id}")
def get_task(task_id: int):
if task_id not in tasks_db:
raise HTTPException(status_code=404, detail="Task not found")
return {"data": tasks_db[task_id]}
@app.delete("/api/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
if task_id not in tasks_db:
raise HTTPException(status_code=404, detail="Task not found")
del tasks_db[task_id]
client = TestClient(app)
@pytest.fixture(autouse=True)
def setup():
tasks_db.clear()
yield
tasks_db.clear()
def test_can_create_and_get_task():
# 建立任務
response = client.post("/api/tasks", json={
"title": "Learn HTTP Testing",
"completed": False
})
assert response.status_code == 201
task_id = response.json()["data"]["id"]
# 取得任務
response = client.get(f"/api/tasks/{task_id}")
assert response.status_code == 200
assert response.json()["data"]["title"] == "Learn HTTP Testing"
def test_validates_task_creation():
# 缺少必要欄位
response = client.post("/api/tasks", json={})
assert response.status_code == 422
# 標題為空
response = client.post("/api/tasks", json={"title": ""})
assert response.status_code == 422
def test_handles_not_found():
response = client.get("/api/tasks/999")
assert response.status_code == 404
assert response.json()["detail"] == "Task not found"
def test_can_delete_task():
# 建立任務
create_response = client.post("/api/tasks", json={
"title": "Task to Delete"
})
task_id = create_response.json()["data"]["id"]
# 刪除任務
response = client.delete(f"/api/tasks/{task_id}")
assert response.status_code == 204
# 確認已刪除
get_response = client.get(f"/api/tasks/{task_id}")
assert get_response.status_code == 404
# 建立 tests/day18/test_best_practices.py
import pytest
def test_handles_invalid_json():
response = client.post(
"/api/users",
data="{invalid json}",
headers={"Content-Type": "application/json"}
)
assert response.status_code == 422
def test_handles_missing_fields():
response = client.post("/api/users", json={"email": "test@example.com"})
assert response.status_code == 422
@pytest.fixture(autouse=True)
def reset_data():
"""確保每個測試都有乾淨的環境"""
users_db.clear()
yield
users_db.clear()
def test_data_isolation():
# 每個測試都應該有乾淨的狀態
assert len(users_db) == 0
試著實作這些功能的測試:
今天我們學習了 Python FastAPI HTTP 測試的基礎:
HTTP 測試是 API 開發的基石,讓我們能在不啟動伺服器的情況下,完整測試 API 的行為。
明天我們將學習更多框架特定的測試技巧,包括如何測試更複雜的應用場景。準備好繼續深入探索了嗎?明天見! 🚀