「資料已經建立了,但客戶說要改...」這是每個開發者的日常。今天我們要完善 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 循環,讓資料的生命週期更加完整。
在開始之前,先思考更新與刪除的測試案例:
讓我們從更新功能開始:
# 建立 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'
]);
});
執行測試會失敗,因為我們還沒有實作更新路由。
現在來實作更新功能:
# 更新 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 的路由模型綁定會自動處理!
現在來實作刪除功能:
# 建立 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
]);
});
實作刪除功能:
# 更新 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 循環:
assertDatabaseHas
驗證更新assertDatabaseMissing
驗證刪除sometimes
修飾符試著實作以下功能:
提示:可以參考今天的實作方式!
恭喜你完成了完整的 Todo API!我們已經實作了:
明天我們將學習如何測試 API 的錯誤處理與驗證規則,讓 API 更加健壯!
「每一個成功的刪除,都是為了更好的開始」- 測試工程師的哲學