基礎測試 [✅] Kata 練習 [✅] 框架實戰 [✅] 整合部署 [🔄]
Day 1-10 Day 11-17 Day 18-27 Day 28-30
↑ 我們在這裡!
「單元測試都過了,為什麼整合起來還是壞掉?」資深工程師搖搖頭,「因為你只測試了零件,沒測試組裝。」
今天,我們要用過去 28 天學到的所有 TDD 技巧,打造一個真正可靠的 Todo API 整合測試套件!
// 建立 tests/Feature/Day29/TodoIntegrationTest.php
<?php
use App\Models\Todo;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->actingAs($this->user);
});
test('completeTodoWorkflow', function () {
// 創建 Todo
$createResponse = $this->postJson('/api/todos', [
'title' => 'Buy groceries',
'description' => 'Milk, Eggs, Bread'
]);
$createResponse->assertStatus(201)
->assertJsonStructure([
'data' => ['id', 'title', 'description', 'completed', 'created_at']
]);
$todoId = $createResponse->json('data.id');
// 檢查列表中是否存在
$listResponse = $this->getJson('/api/todos');
$listResponse->assertStatus(200)
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $todoId);
// 標記完成
$completeResponse = $this->patchJson("/api/todos/{$todoId}/complete");
$completeResponse->assertStatus(200)
->assertJsonPath('data.completed', true);
// 刪除
$deleteResponse = $this->deleteJson("/api/todos/{$todoId}");
$deleteResponse->assertStatus(204);
// 確認已刪除
$this->getJson('/api/todos')
->assertJsonCount(0, 'data');
});
// 更新 tests/Feature/Day29/TodoIntegrationTest.php
test('handlesValidationErrors', function () {
// 空標題
$this->postJson('/api/todos', ['title' => ''])
->assertStatus(422)
->assertJsonValidationErrors(['title']);
// 標題太長
$longTitle = str_repeat('a', 256);
$this->postJson('/api/todos', ['title' => $longTitle])
->assertStatus(422)
->assertJsonValidationErrors(['title']);
// 不存在的 Todo
$this->patchJson('/api/todos/99999/complete')
->assertStatus(404);
// 未授權存取
$this->withoutMiddleware();
Auth::logout();
$this->getJson('/api/todos')
->assertStatus(401);
});
test('handlesConcurrentUpdates', function () {
$todo = Todo::factory()->create(['user_id' => $this->user->id]);
// 模擬兩個請求同時更新
$response1 = $this->patchJson("/api/todos/{$todo->id}", [
'title' => 'Updated Title 1'
]);
$response2 = $this->patchJson("/api/todos/{$todo->id}", [
'title' => 'Updated Title 2'
]);
// 兩個請求都應該成功
$response1->assertStatus(200);
$response2->assertStatus(200);
// 最後一個更新應該生效
$todo->refresh();
expect($todo->title)->toBe('Updated Title 2');
});
// 更新 tests/Feature/Day29/TodoIntegrationTest.php
test('handles large dataset efficiently', function () {
// 建立大量測試資料
Todo::factory()->count(100)
->create(['user_id' => $this->user->id]);
$startTime = microtime(true);
$response = $this->getJson('/api/todos?per_page=50');
$executionTime = (microtime(true) - $startTime) * 1000;
$response->assertStatus(200)
->assertJsonCount(50, 'data');
// 確保回應時間在合理範圍內
expect($executionTime)->toBeLessThan(500); // 500ms
});
test('searchWithSpecialCharacters', function () {
Todo::factory()->createMany([
['user_id' => $this->user->id, 'title' => 'Buy milk & eggs'],
['user_id' => $this->user->id, 'title' => 'Meeting @ 3pm'],
['user_id' => $this->user->id, 'title' => 'Call John (urgent)']
]);
// 測試特殊字元搜尋
$this->getJson('/api/todos?search=' . urlencode('&'))
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.title', 'Buy milk & eggs');
});
// 建立 tests/Feature/Day29/ApiVersioningTest.php
<?php
use App\Models\Todo;
use App\Models\User;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->actingAs($this->user);
Todo::factory()->create(['user_id' => $this->user->id]);
});
test('apiVersionsReturnDifferentStructures', function () {
// V1 API - 基礎結構
$this->getJson('/api/v1/todos')
->assertStatus(200)
->assertJsonStructure([
'data' => [['id', 'title', 'completed']]
]);
// V2 API - 增強結構
$this->getJson('/api/v2/todos')
->assertStatus(200)
->assertJsonStructure([
'data' => [['id', 'title', 'description', 'completed', 'priority']],
'meta' => ['version']
]);
});
// 更新 tests/Feature/Day29/ApiVersioningTest.php
test('handlesRateLimiting', function () {
// 快速發送多個請求
for ($i = 0; $i < 60; $i++) {
$this->getJson('/api/todos');
}
// 第 61 個請求應該被限制
$this->getJson('/api/todos')
->assertStatus(429)
->assertHeader('X-RateLimit-Remaining', 0);
});
test('transactionRollbackOnBatchError', function () {
$initialCount = Todo::count();
// 嘗試批次建立,其中一個會失敗
$this->postJson('/api/todos/batch', [
'todos' => [
['title' => 'Valid Todo 1'],
['title' => ''], // 驗證失敗
['title' => 'Valid Todo 3']
]
])->assertStatus(422);
// 確認交易回滾
expect(Todo::count())->toBe($initialCount);
});
// 建立 tests/Feature/Day29/CrossServiceTest.php
<?php
use App\Models\User;
use App\Models\Todo;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->actingAs($this->user);
});
test('highPriorityTodoTriggersNotification', function () {
Queue::fake();
$this->postJson('/api/todos', [
'title' => 'Important Task',
'priority' => 'high'
])->assertStatus(201);
Queue::assertPushed(\App\Jobs\SendTodoNotification::class);
});
test('bulkCompleteMaintainsConsistency', function () {
$todos = Todo::factory()->count(5)->create([
'user_id' => $this->user->id,
'completed' => false
]);
$this->patchJson('/api/todos/bulk-complete', [
'ids' => $todos->pluck('id')->toArray()
])->assertJson(['updated' => 5]);
// 驗證所有 Todo 都已完成
$todos->each(fn($todo) => expect($todo->fresh()->completed)->toBeTrue());
});
// 完整實作 tests/Feature/Day29/TestPyramidValidation.php
<?php
test('testPyramidIsBalanced', function () {
$unitTests = collect(File::allFiles(base_path('tests/Unit')))
->filter(fn($file) => str_ends_with($file->getFilename(), 'Test.php'))
->count();
$featureTests = collect(File::allFiles(base_path('tests/Feature')))
->filter(fn($file) => str_ends_with($file->getFilename(), 'Test.php'))
->count();
// 單元測試應該比功能測試多,但比例合理
expect($unitTests)->toBeGreaterThan($featureTests);
expect($unitTests / max($featureTests, 1))->toBeLessThan(3);
});
test('criticalPathsHaveTests', function () {
$criticalPaths = [
'POST /api/todos',
'GET /api/todos',
'PATCH /api/todos/{id}'
];
foreach ($criticalPaths as $path) {
[$method, $uri] = explode(' ', $path);
$route = Route::getRoutes()->match(Request::create($uri, $method));
expect($route)->not->toBeNull();
}
});
恭喜你!我們已經完成了完整的整合測試實作。明天是我們 30 天旅程的最後一天,我們將把所有學到的知識整合起來,進行最終部署,並回顧這段 TDD 學習之旅的精彩時刻。
今天我們實作了完整的整合測試套件,涵蓋了:
這些測試確保了我們的 API 在各種情況下都能正確運作,為前後端協作提供了堅實的基礎。
記住:好的整合測試就像是系統的守護者,它確保所有組件能夠和諧地協同工作!