iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0

「資料已經建立了,但客戶說要改...」這是每個開發者的日常。今天我們要完善 Todo API 的最後兩個功能:更新與刪除。透過 TDD 的方式,確保這些關鍵操作都能正確執行!

本日旅程地圖 🗺️

基礎測試概念 (Days 1-10)
    ├── Day 1: 什麼是 TDD?
    ├── Day 2: 認識斷言
    ├── Day 3: 紅綠重構循環
    ├── Day 4: 單元測試基礎
    ├── Day 5: 測試生命週期
    ├── Day 6: AAA 模式
    ├── Day 7: 測試替身基礎
    ├── Day 8: 參數化測試
    ├── Day 9: 測試覆蓋率
    └── Day 10: 基礎回顧與小結

Roman Numeral Kata (Days 11-17)
    ├── Day 11: Kata 介紹與起步
    ├── Day 12: 建立測試結構
    ├── Day 13: 基礎轉換(I, V, X)
    ├── Day 14: 進階數字轉換
    ├── Day 15: 反向轉換
    ├── Day 16: 錯誤處理
    └── Day 17: 重構與最佳化

框架特色開發 (Days 18-27)
    ├── Day 18: pytest 測試框架進階
    ├── Day 19: FastAPI 測試基礎
    ├── Day 20: 資料驅動測試  
    ├── Day 21: 建立與讀取測試
    └── Day 22: 測試更新與刪除 📍 我們在這裡!

今天我們要完成 Todo API 的 CRUD 循環,讓資料的生命週期更加完整。

測試策略規劃

在開始之前,先思考更新與刪除的測試案例:

更新 Todo 的測試案例

  • 成功更新 Todo 的標題
  • 切換 Todo 的完成狀態
  • 更新不存在的 Todo 應回傳 404
  • 驗證資料確實被更新

刪除 Todo 的測試案例

  • 成功刪除 Todo
  • 刪除不存在的 Todo 應回傳 404
  • 驗證資料確實被刪除
  • 刪除後無法再次查詢

更新 Todo - 紅燈階段 🔴

讓我們從更新功能開始:

建立 tests/day22/test_todo_update.py

import pytest
from fastapi.testclient import TestClient
from src.todo.app import app
from src.todo.models import TodoInDB, TodoUpdate
from src.todo.database import get_db, clear_db

client = TestClient(app)

def setup_function():
    """每個測試前清空資料庫"""
    clear_db()

def test_update_todo():
    # Arrange: 先建立一個 todo
    create_response = client.post(
        "/todos/",
        json={"title": "原始標題", "completed": False}
    )
    todo_id = create_response.json()["id"]
    
    # Act: 更新 todo
    update_response = client.put(
        f"/todos/{todo_id}",
        json={"title": "更新後的標題", "completed": True}
    )
    
    # Assert
    assert update_response.status_code == 200
    updated_todo = update_response.json()
    assert updated_todo["title"] == "更新後的標題"
    assert updated_todo["completed"] is True
    assert updated_todo["id"] == todo_id

更新 Todo - 綠燈階段 🟢

現在來實作更新功能:

更新 src/todo/app.py

from fastapi import FastAPI, HTTPException
from typing import List, Optional
from .models import TodoCreate, TodoInDB, TodoUpdate
from .database import get_db

app = FastAPI()

@app.put("/todos/{todo_id}", response_model=TodoInDB)
def update_todo(todo_id: str, todo_update: TodoUpdate):
    db = get_db()
    
    # 找出要更新的 todo
    todo_index = None
    for i, todo in enumerate(db.todos):
        if todo.id == todo_id:
            todo_index = i
            break
    
    if todo_index is None:
        raise HTTPException(status_code=404, detail="Todo not found")
    
    # 更新資料
    existing_todo = db.todos[todo_index]
    update_data = todo_update.dict(exclude_unset=True)
    updated_todo = existing_todo.copy(update=update_data)
    db.todos[todo_index] = updated_todo
    
    return updated_todo

測試切換完成狀態

建立 tests/day22/test_partial_update.py

def test_partial_update_title_only():
    # Arrange
    create_response = client.post(
        "/todos/",
        json={"title": "原始標題", "completed": False}
    )
    todo_id = create_response.json()["id"]
    
    # Act: 只更新標題
    update_response = client.patch(
        f"/todos/{todo_id}",
        json={"title": "只更新標題"}
    )
    
    # Assert
    assert update_response.status_code == 200
    updated_todo = update_response.json()
    assert updated_todo["title"] == "只更新標題"
    assert updated_todo["completed"] is False  # 應該保持不變

def test_partial_update_completed_only():
    # Arrange
    create_response = client.post(
        "/todos/",
        json={"title": "保持標題", "completed": False}
    )
    todo_id = create_response.json()["id"]
    
    # Act: 只更新完成狀態
    update_response = client.patch(
        f"/todos/{todo_id}",
        json={"completed": True}
    )
    
    # Assert
    assert update_response.status_code == 200
    updated_todo = update_response.json()
    assert updated_todo["title"] == "保持標題"  # 應該保持不變
    assert updated_todo["completed"] is True

刪除 Todo - 紅燈階段

現在來實作刪除功能:

建立 tests/day22/test_todo_delete.py

def test_delete_todo():
    # Arrange
    create_response = client.post(
        "/todos/",
        json={"title": "即將被刪除", "completed": False}
    )
    todo_id = create_response.json()["id"]
    
    # Act
    delete_response = client.delete(f"/todos/{todo_id}")
    
    # Assert
    assert delete_response.status_code == 204
    
    # 驗證已經被刪除
    get_response = client.get(f"/todos/{todo_id}")
    assert get_response.status_code == 404

def test_delete_nonexistent_todo():
    # Act & Assert
    response = client.delete("/todos/nonexistent-id")
    assert response.status_code == 404

刪除 Todo - 綠燈階段

實作刪除功能:

更新 src/todo/app.py

from fastapi import FastAPI, HTTPException, status
from fastapi.responses import Response

@app.delete("/todos/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_todo(todo_id: str):
    db = get_db()
    
    # 找出要刪除的 todo
    todo_index = None
    for i, todo in enumerate(db.todos):
        if todo.id == todo_id:
            todo_index = i
            break
    
    if todo_index is None:
        raise HTTPException(status_code=404, detail="Todo not found")
    
    # 刪除 todo
    db.todos.pop(todo_index)
    return Response(status_code=status.HTTP_204_NO_CONTENT)

測試錯誤情境

我們也需要處理更新不存在的 Todo:

建立 tests/day22/test_edge_cases.py

def test_update_nonexistent_todo():
    # Act & Assert
    response = client.put(
        "/todos/nonexistent-id",
        json={"title": "不會成功", "completed": True}
    )
    assert response.status_code == 404
    assert response.json()["detail"] == "Todo not found"

def test_update_with_invalid_data():
    # Arrange
    create_response = client.post(
        "/todos/",
        json={"title": "測試項目", "completed": False}
    )
    todo_id = create_response.json()["id"]
    
    # Act & Assert: 測試空標題
    response = client.put(
        f"/todos/{todo_id}",
        json={"title": "", "completed": True}
    )
    assert response.status_code == 422

本日學習重點 📝

今天我們完成了 Todo API 的 CRUD 循環:

完成的功能

  1. 更新 Todo:支援部分更新與完整更新
  2. 刪除 Todo:確保資料確實被移除
  3. 完整生命週期:從建立到刪除的完整測試

測試技巧

  • 使用 HTTPException 處理 404 錯誤
  • 測試完整的資料生命週期
  • 處理錯誤情境(404)

Python 特色

  • Pydantic 的部分更新 (exclude_unset)
  • FastAPI 的路徑參數綁定
  • HTTP 動詞對應 CRUD 操作
  • 資料驗證器 (validators)

小挑戰 🏆

試著實作以下功能:

  1. 批次刪除:一次刪除多個 Todo
  2. 軟刪除:不真的刪除資料,只是標記
  3. 部分更新驗證:確保至少提供一個欄位

提示:可以參考今天的實作方式!

總結

恭喜你完成了完整的 Todo API!我們已經實作了:

  • Create(建立)- Day 20
  • Read(讀取)- Day 21
  • Update(更新)- Day 22
  • Delete(刪除)- Day 22

明天我們將學習如何測試 API 的錯誤處理與驗證規則,讓 API 更加健壯!


「每一個成功的刪除,都是為了更好的開始」- 測試工程師的哲學


上一篇
Day 21 - 測試新增 Todo ➕
下一篇
Day 23 - 測試篩選與路由 🎯
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言