「為什麼待辦事項總是越來越多?」PM 看著滿滿的 backlog 嘆氣。
「因為我們的 TodoList 還沒測試完啊!」我笑著回答。
今天我們要用 TDD 打造一個完整的 TodoList 元件,這不只是一個練習,而是每個開發者都會遇到的實戰場景。
基礎觀念 ✅ → 進階技巧 ✅ → Kata 實戰 ✅ → 【框架應用 📍】 → 下階段 → 最終章
經過前 19 天的訓練,我們已經準備好挑戰真實世界的應用了!
在開始寫測試前,先定義我們的 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',
];
}
試試看能不能完成這些進階功能:
提示:記得先寫測試!
明天我們將學習如何為這個 TodoList 加上持久化儲存功能!
小測驗:如果要加入「批次刪除」功能,你會怎麼設計測試案例?