iT邦幫忙

2025 iThome 鐵人賽

DAY 29
0

今日旅程地圖

基礎測試 [✅]        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');
});

完整的 Todo App 整合測試

// 更新 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 版本控制
  • 錯誤處理機制
  • 跨服務整合

這些測試確保了我們的 API 在各種情況下都能正確運作,為前後端協作提供了堅實的基礎。

記住:好的整合測試就像是系統的守護者,它確保所有組件能夠和諧地協同工作!


上一篇
Day 28 - 整合準備 🔧
下一篇
Day 30 - 部署與總結 🎊
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言