iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0

「為什麼待辦事項總是越來越多?」PM 看著滿滿的 backlog 嘆氣。
「因為我們的 TodoList 還沒測試完啊!」我笑著回答。

今天我們要用 TDD 打造一個完整的 TodoList 元件,這不只是一個練習,而是每個開發者都會遇到的實戰場景。

🗺️ 我們的 TDD 旅程

基礎觀念 ✅ → 進階技巧 ✅ → Kata 實戰 ✅ → 【框架應用 📍】 → 下階段 → 最終章

經過前 19 天的訓練,我們已經準備好挑戰真實世界的應用了!

📋 TodoList 功能規格

在開始寫測試前,先定義我們的 TodoList 需要什麼功能:

功能 描述 優先級
新增項目 輸入文字後按 Enter 新增 🔴 高
顯示列表 顯示所有待辦事項 🔴 高
標記完成 點擊勾選框標記完成 🟡 中
刪除項目 點擊刪除按鈕移除 🟡 中
編輯項目 雙擊文字進入編輯模式 🟢 低

🎯 第一個測試:新增待辦事項

讓我們從最核心的功能開始:

建立 tests/Feature/Day20/TodoListTest.php

<?php

namespace Tests\Feature\Day20;

use App\Models\Todo;
use Tests\TestCase;

test('can_add_todo_item', function () {
    // Arrange
    $todoData = ['title' => '買牛奶'];
    
    // Act
    $response = $this->postJson('/api/todos', $todoData);
    
    // Assert
    $response->assertStatus(201);
    $this->assertDatabaseHas('todos', $todoData);
});

執行測試,紅燈!這正是 TDD 的第一步。

🔧 實作最小可行版本

建立 app/Http/Controllers/TodoController.php

<?php

namespace App\Http\Controllers;

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

class TodoController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
        ]);
        
        $todo = Todo::create($validated);
        
        return response()->json($todo, 201);
    }
    
    public function index()
    {
        return response()->json(Todo::all());
    }
}

測試通過!綠燈亮起 ✅

🧪 測試邊界條件

測試空值與驗證規則

test('cannot_add_empty_todo', function () {
    $response = $this->postJson('/api/todos', ['title' => '']);
    $response->assertStatus(422);
});

test('can_add_multiple_todos', function () {
    $todos = [
        ['title' => '寫測試'],
        ['title' => '重構程式碼'],
        ['title' => '喝咖啡']
    ];
    
    foreach ($todos as $todo) {
        $this->postJson('/api/todos', $todo);
    }
    
    $this->assertDatabaseCount('todos', 3);
});

✅ 標記完成功能

新增標記完成測試

test('can_mark_todo_as_completed', function () {
    $todo = Todo::factory()->create(['completed' => false]);
    
    $response = $this->patchJson("/api/todos/{$todo->id}", [
        'completed' => true
    ]);
    
    $response->assertStatus(200);
    $this->assertTrue($todo->fresh()->completed);
});

🗑️ 刪除功能

新增刪除測試

test('can_delete_todo', function () {
    $todo = Todo::factory()->create();
    
    $response = $this->deleteJson("/api/todos/{$todo->id}");
    
    $response->assertStatus(204);
    $this->assertDatabaseMissing('todos', ['id' => $todo->id]);
});

✏️ 編輯功能

新增編輯測試

test('can_edit_todo_title', function () {
    $todo = Todo::factory()->create(['title' => '原始文字']);
    
    $response = $this->patchJson("/api/todos/{$todo->id}", [
        'title' => '修改後的文字'
    ]);
    
    $response->assertStatus(200);
    $this->assertEquals('修改後的文字', $todo->fresh()->title);
});

🔍 進階功能:篩選與統計

新增統計測試

test('can_get_statistics', function () {
    Todo::factory()->count(3)->create(['completed' => false]);
    Todo::factory()->count(2)->create(['completed' => true]);
    
    $response = $this->getJson('/api/todos/stats');
    
    $response->assertJson([
        'total' => 5,
        'completed' => 2,
        'pending' => 3,
        'completion_rate' => 40.0
    ]);
});

🎨 完整實作

完整實作 app/Http/Controllers/TodoController.php

<?php

namespace App\Http\Controllers;

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

class TodoController extends Controller
{
    public function index()
    {
        return response()->json(Todo::all());
    }
    
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
        ]);
        
        $todo = Todo::create($validated);
        
        return response()->json($todo, 201);
    }
    
    public function update(Request $request, Todo $todo)
    {
        $validated = $request->validate([
            'title' => 'sometimes|string|max:255',
            'completed' => 'sometimes|boolean'
        ]);
        
        $todo->update($validated);
        
        return response()->json($todo);
    }
    
    public function destroy(Todo $todo)
    {
        $todo->delete();
        
        return response()->noContent();
    }
    
    public function stats()
    {
        $total = Todo::count();
        $completed = Todo::where('completed', true)->count();
        
        return response()->json([
            'total' => $total,
            'completed' => $completed,
            'pending' => $total - $completed,
            'completion_rate' => $total > 0 ? ($completed / $total * 100) : 0
        ]);
    }
}

建立 app/Models/Todo.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Todo extends Model
{
    use HasFactory;
    
    protected $fillable = ['title', 'completed'];
    
    protected $casts = [
        'completed' => 'boolean',
    ];
}

💡 小挑戰時間

試試看能不能完成這些進階功能:

  1. 優先級排序:讓待辦事項可以設定優先級
  2. 到期日提醒:加入到期日功能
  3. 標籤分類:為待辦事項加上標籤

提示:記得先寫測試!

🎯 今日重點回顧

  • ✅ 用 TDD 完成 TodoList 的核心功能
  • ✅ 實作 CRUD 操作(新增、讀取、更新、刪除)
  • ✅ 加入統計與篩選功能
  • ✅ 透過重構讓程式碼更優雅

明天我們將學習如何為這個 TodoList 加上持久化儲存功能!


小測驗:如果要加入「批次刪除」功能,你會怎麼設計測試案例?


上一篇
Day 19 - 資料庫測試設置 🗄️
下一篇
Day 21 - 測試新增 Todo ➕
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言