iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0

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

本日旅程地圖 🗺️

基礎期(✅ 已完成)
├── Day 1-3: 測試入門
├── Day 4-6: 進階概念  
├── Day 7-10: 重構技巧
│
TDD 實踐期(✅ 已完成)
├── Day 11-17: Roman Numeral Kata
│
框架實戰期(進行中 📍)
├── Day 18: HTTP 測試基礎
├── Day 19: 資料模型測試
├── Day 20: 建立 Todo
├── Day 21: 列表與查詢
├── Day 22: 更新與刪除 ← 我們在這裡!
└── Day 23-27: 待解鎖...

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

測試策略規劃 📋

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

更新 Todo 的測試案例

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

刪除 Todo 的測試案例

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

更新 Todo - 紅燈階段 🔴

讓我們從更新功能開始:

# 建立 tests/Feature/Day22/TodoUpdateTest.php
<?php

use App\Models\Todo;
use function Pest\Laravel\putJson;

test('can_update_todo_title', function () {
    // Arrange
    $todo = Todo::create([
        'title' => 'Original title',
        'completed' => false
    ]);
    
    // Act
    $response = putJson("/api/todos/{$todo->id}", [
        'title' => 'Updated title'
    ]);
    
    // Assert
    $response->assertStatus(200)
        ->assertJson([
            'id' => $todo->id,
            'title' => 'Updated title',
            'completed' => false
        ]);
    
    // 驗證資料庫
    $this->assertDatabaseHas('todos', [
        'id' => $todo->id,
        'title' => 'Updated title'
    ]);
});

執行測試會失敗,因為我們還沒有實作更新路由。

更新 Todo - 綠燈階段 🟢

現在來實作更新功能:

# 更新 routes/api.php
<?php

use App\Http\Controllers\TodoController;
use Illuminate\Support\Facades\Route;

Route::get('todos', [TodoController::class, 'index']);
Route::post('todos', [TodoController::class, 'store']);
Route::get('todos/{todo}', [TodoController::class, 'show']);
Route::put('todos/{todo}', [TodoController::class, 'update']);  // 新增這行
# 更新 app/Http/Controllers/TodoController.php
<?php

namespace App\Http\Controllers;

use App\Models\Todo;
use Illuminate\Http\Request;

class TodoController extends Controller
{
    // ... 其他方法保持不變

    public function update(Request $request, Todo $todo)
    {
        $validated = $request->validate([
            'title' => 'sometimes|required|string|max:255',
            'completed' => 'sometimes|boolean'
        ]);
        
        $todo->update($validated);
        
        return response()->json($todo);
    }
}

現在測試應該通過了!讓我們繼續測試其他更新場景。

測試切換完成狀態 ✅

# 更新 tests/Feature/Day22/TodoUpdateTest.php
test('can_toggle_todo_completed_status', function () {
    // Arrange
    $todo = Todo::create([
        'title' => 'Test todo',
        'completed' => false
    ]);
    
    // Act - 標記為完成
    $response = putJson("/api/todos/{$todo->id}", [
        'completed' => true
    ]);
    
    // Assert
    $response->assertStatus(200)
        ->assertJson([
            'id' => $todo->id,
            'title' => 'Test todo',
            'completed' => true
        ]);
    
    // Act - 再次標記為未完成
    $response = putJson("/api/todos/{$todo->id}", [
        'completed' => false
    ]);
    
    // Assert
    $response->assertJson([
        'completed' => false
    ]);
});

測試錯誤情境 ❌

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

# 更新 tests/Feature/Day22/TodoUpdateTest.php
test('returns_404_when_updating_non_existent_todo', function () {
    // Act
    $response = putJson('/api/todos/999', [
        'title' => 'Updated title'
    ]);
    
    // Assert
    $response->assertStatus(404);
});

這個測試已經通過了,因為 Laravel 的路由模型綁定會自動處理!

刪除 Todo - 紅燈階段 🔴

現在來實作刪除功能:

# 建立 tests/Feature/Day22/TodoDeleteTest.php
<?php

use App\Models\Todo;
use function Pest\Laravel\deleteJson;

test('can_delete_todo', function () {
    // Arrange
    $todo = Todo::create([
        'title' => 'To be deleted',
        'completed' => false
    ]);
    
    // Act
    $response = deleteJson("/api/todos/{$todo->id}");
    
    // Assert
    $response->assertStatus(204);
    
    // 驗證資料庫
    $this->assertDatabaseMissing('todos', [
        'id' => $todo->id
    ]);
});

刪除 Todo - 綠燈階段 🟢

實作刪除功能:

# 更新 routes/api.php
Route::delete('todos/{todo}', [TodoController::class, 'destroy']);
# 更新 app/Http/Controllers/TodoController.php
public function destroy(Todo $todo)
{
    $todo->delete();
    
    return response()->noContent();
}

測試連續操作 🔄

讓我們測試一個完整的生命週期:

# 建立 tests/Feature/Day22/TodoLifecycleTest.php
<?php

use function Pest\Laravel\{postJson, getJson, putJson, deleteJson};

test('complete_todo_lifecycle', function () {
    // 建立
    $createResponse = postJson('/api/todos', [
        'title' => 'Lifecycle test'
    ]);
    $todoId = $createResponse->json('id');
    
    // 更新
    putJson("/api/todos/{$todoId}", [
        'title' => 'Updated lifecycle test',
        'completed' => true
    ])->assertStatus(200);
    
    // 刪除
    deleteJson("/api/todos/{$todoId}")
        ->assertStatus(204);
    
    // 驗證刪除結果
    getJson("/api/todos/{$todoId}")
        ->assertStatus(404);
});

本日學習重點 📝

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

🎯 完成的功能

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

💡 測試技巧

  • 使用 assertDatabaseHas 驗證更新
  • 使用 assertDatabaseMissing 驗證刪除
  • 測試完整的資料生命週期
  • 處理錯誤情境(404)

🔧 Laravel 特色

  • 路由模型綁定自動處理 404
  • Eloquent 的批次更新功能
  • HTTP 動詞對應 CRUD 操作
  • 驗證規則的 sometimes 修飾符

小挑戰 🏆

試著實作以下功能:

  1. 批次刪除:一次刪除多個 Todo
  2. 軟刪除:使用 Laravel 的 SoftDeletes
  3. 部分更新驗證:確保至少提供一個欄位

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

總結 🎊

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

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

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


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


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

尚未有邦友留言

立即登入留言