iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0

昨天我們完成了 Roman Numeral Kata,現在進入框架特定測試的新階段!想像一個場景:專案上線前夕,PM 緊張地問:「API 都測試過了嗎?」你自信地回答:「每個端點都有完整的測試覆蓋!」這就是今天要學習的 HTTP 測試。

本日學習地圖 🗺️

基礎階段             Kata 階段            框架特定測試
Days 1-10           Days 11-17           Days 18-27
   ✅                  ✅                  📍 Day 18

                                        [HTTP 測試基礎] <- 今天在這
                                              ⬇️
                                        下一階段:更多測試技巧

為什麼需要 HTTP 測試? 🤔

在 Python 中,HTTP 測試允許我們:

  • 測試完整的請求生命週期:從請求到回應的完整流程
  • 驗證路由、中間件、處理函數的整合:確保各層級正確協作
  • 確保 API 回應符合預期:狀態碼、資料格式、錯誤處理
  • 不需啟動真實伺服器:測試執行更快速、更穩定

HTTP 測試 vs 單元測試

面向 單元測試 HTTP 測試
測試範圍 單一函數或類別 完整請求流程
執行速度 極快
測試重點 邏輯正確性 整合正確性
覆蓋範圍 細節實作 使用者體驗

環境準備 🛠️

# 安裝 FastAPI 和測試工具
pip install fastapi httpx pytest-asyncio

FastAPI HTTP 測試基礎

1. 第一個 HTTP 測試

# 建立 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"}

2. 測試不同 HTTP 方法

# 建立 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

3. JSON 資料驗證

# 建立 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)

實戰範例:Task API 測試 ⚡

# 建立 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

小挑戰 🎯

試著實作這些功能的測試:

  1. 查詢參數測試:測試不同查詢參數組合
  2. 錯誤情況測試:測試各種異常狀況
  3. 資料隔離測試:確保測試間不會相互影響

本日重點回顧 📝

今天我們學習了 Python FastAPI HTTP 測試的基礎:

  1. 基本請求方法:GET、POST、PUT、DELETE
  2. JSON 測試:發送和驗證 JSON 資料
  3. 回應驗證:狀態碼、內容、結構
  4. 完整 CRUD 測試:實作 Task API 測試
  5. 進階技巧:錯誤處理、資料隔離

HTTP 測試是 API 開發的基石,讓我們能在不啟動伺服器的情況下,完整測試 API 的行為。

關鍵要點總結 🔑

  • FastAPI TestClient:提供完整的測試環境,無需真實伺服器
  • 請求與回應:能測試各種 HTTP 方法和資料格式
  • 錯誤處理:確保 API 能優雅地處理各種異常情況
  • 測試隔離:每個測試都應該獨立,不相互影響
  • 測試覆蓋:涵蓋正常流程和異常情況

明天我們將學習更多框架特定的測試技巧,包括如何測試更複雜的應用場景。準備好繼續深入探索了嗎?明天見! 🚀


上一篇
Day 17 - 程式碼整理與回顧 🏁
下一篇
Day 19 - 資料庫測試設置 🗄️
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言