iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0

「前端寫好了,後端也測試通過了,為什麼整合起來就是不能動?」

資深工程師微笑著說:「你有測試過前後端的合作默契嗎?」

經過 27 天的旅程,我們已經具備了扎實的測試基礎。今天,我們要為即將到來的前後端整合做準備。

旅程回顧與定位 🗺️

基礎測試 ✅ → Kata 實戰 ✅ → 框架測試 ✅ → 【整合準備 📍】
Day 1-10      Day 11-17     Day 18-27      Day 28 ← 我們在這裡

整合前的健康檢查 🏥

建立 tests/Feature/Day28/IntegrationChecklistTest.php:

<?php

namespace Tests\Feature\Day28;

use Tests\TestCase;
use App\Models\Todo;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

test('verify_all_api_endpoints_are_working', function () {
    // GET /api/todos
    $this->getJson('/api/todos')
         ->assertStatus(200)
         ->assertJsonStructure([
             'data' => ['*' => ['id', 'title', 'completed']]
         ]);
    
    // POST /api/todos
    $response = $this->postJson('/api/todos', ['title' => 'Test Todo'])
                     ->assertStatus(201);
    $todoId = $response->json('data.id');
    
    // PATCH /api/todos/{id}
    $this->patchJson("/api/todos/{$todoId}", ['completed' => true])
         ->assertStatus(200)
         ->assertJsonFragment(['completed' => true]);
    
    // DELETE /api/todos/{id}
    $this->deleteJson("/api/todos/{$todoId}")->assertStatus(204);
    $this->assertDatabaseMissing('todos', ['id' => $todoId]);
});

test('api_responses_follow_consistent_format', function () {
    Todo::factory()->create(['title' => 'Test Todo']);
    
    $this->getJson('/api/todos')
         ->assertJsonStructure([
             'data' => [],
             'meta' => ['total', 'page', 'per_page']
         ]);
});

test('error_responses_have_consistent_structure', function () {
    $this->postJson('/api/todos', [])
         ->assertStatus(422)
         ->assertJsonStructure([
             'message',
             'errors' => ['title' => []]
         ]);
});

CORS 設定與驗證 🌐

建立 tests/Feature/Day28/CorsTest.php:

<?php

namespace Tests\Feature\Day28;

test('cors_headers_are_present', function () {
    $response = $this->options('/api/todos', [], [
        'Origin' => 'http://localhost:3000',
        'Access-Control-Request-Method' => 'GET'
    ]);
    
    $response->assertHeader('Access-Control-Allow-Origin')
             ->assertHeader('Access-Control-Allow-Methods');
});

test('preflight_requests_work_correctly', function () {
    $response = $this->options('/api/todos', [], [
        'Origin' => 'http://localhost:3000',
        'Access-Control-Request-Method' => 'POST',
        'Access-Control-Request-Headers' => 'Content-Type'
    ]);
    
    $response->assertStatus(204)
             ->assertHeader('Access-Control-Allow-Headers');
});

認證與授權準備 🔐

建立 tests/Feature/Day28/AuthenticationReadinessTest.php:

<?php

namespace Tests\Feature\Day28;

use App\Models\User;
use Laravel\Sanctum\Sanctum;

test('authenticated_requests_work', function () {
    $user = User::factory()->create();
    Sanctum::actingAs($user);
    
    $this->getJson('/api/user')
         ->assertOk()
         ->assertJson(['id' => $user->id]);
});

test('unauthenticated_requests_are_rejected', function () {
    $this->getJson('/api/user')
         ->assertStatus(401)
         ->assertJson(['message' => 'Unauthenticated.']);
});

test('token_based_auth_works', function () {
    $user = User::factory()->create();
    $token = $user->createToken('test-token')->plainTextToken;
    
    $this->withHeaders(['Authorization' => "Bearer {$token}"])
         ->getJson('/api/user')
         ->assertOk();
});

資料驗證邊界測試 🛡️

建立 tests/Feature/Day28/ValidationBoundaryTest.php:

<?php

namespace Tests\Feature\Day28;

test('validates_string_length_limits', function () {
    $longString = str_repeat('a', 256);
    
    $this->postJson('/api/todos', ['title' => $longString])
         ->assertStatus(422)
         ->assertJsonValidationErrors(['title']);
});

test('validates_special_characters', function () {
    $specialChars = '<script>alert("XSS")</script>';
    
    $response = $this->postJson('/api/todos', ['title' => $specialChars])
                     ->assertStatus(201);
    
    // 確保特殊字符被正確處理
    expect($response->json('data.title'))->toBe($specialChars);
});

test('validates_unicode_characters', function () {
    $unicodeTitle = '測試 📝 テスト 🎯';
    
    $this->postJson('/api/todos', ['title' => $unicodeTitle])
         ->assertStatus(201)
         ->assertJsonPath('data.title', $unicodeTitle);
});

效能基準測試 ⚡

建立 tests/Feature/Day28/PerformanceBenchmarkTest.php:

<?php

namespace Tests\Feature\Day28;

use App\Models\Todo;

test('api_response_time_is_acceptable', function () {
    Todo::factory(10)->create();
    
    $start = microtime(true);
    $this->getJson('/api/todos')->assertOk();
    $duration = (microtime(true) - $start) * 1000;
    
    expect($duration)->toBeLessThan(100); // 小於 100ms
});

test('handles_concurrent_requests', function () {
    $responses = [];
    
    for ($i = 0; $i < 5; $i++) {
        $responses[] = $this->postJson('/api/todos', [
            'title' => "Concurrent Todo {$i}"
        ]);
    }
    
    foreach ($responses as $response) {
        $response->assertStatus(201);
    }
    
    $this->assertDatabaseCount('todos', 5);
});

錯誤恢復測試 🚨

建立 tests/Feature/Day28/ErrorRecoveryTest.php:

<?php

namespace Tests\Feature\Day28;

use Illuminate\Support\Facades\DB;

test('recovers_from_database_connection_failure', function () {
    // 暫時斷開資料庫連接
    DB::disconnect();
    
    $response = $this->getJson('/api/health');
    $response->assertStatus(503);
    
    // 重新連接
    DB::reconnect();
    
    $response = $this->getJson('/api/health');
    $response->assertOk();
});

test('handles_malformed_json_requests', function () {
    $response = $this->call('POST', '/api/todos', [], [], [], [
        'HTTP_CONTENT_TYPE' => 'application/json'
    ], '{invalid json}');
    
    $response->assertStatus(400)
            ->assertJsonFragment(['message' => 'Invalid JSON']);
});

完整實作:整合檢查服務

完整實作 app/Services/IntegrationChecker.php:

<?php

namespace App\Services;

class IntegrationChecker
{
    private array $checks = [];
    
    public function checkDatabase(): bool
    {
        try {
            \DB::connection()->getPdo();
            $this->checks['database'] = true;
            return true;
        } catch (\Exception $e) {
            $this->checks['database'] = false;
            return false;
        }
    }
    
    public function checkCache(): bool
    {
        try {
            cache(['test' => 'value'], 1);
            $result = cache('test') === 'value';
            $this->checks['cache'] = $result;
            return $result;
        } catch (\Exception $e) {
            $this->checks['cache'] = false;
            return false;
        }
    }
    
    public function getAllChecks(): array
    {
        return $this->checks;
    }
}

今天的成就總結 🎉

✅ 建立完整的 API 端點健康檢查
✅ 設定並測試 CORS 配置
✅ 準備認證與授權基礎設施
✅ 實作資料驗證邊界測試
✅ 建立效能基準測試
✅ 測試錯誤恢復機制
✅ 驗證 API 文件完整性

本日重點回顧

今天我們為前後端整合做了充分的準備:

  1. 健康檢查:確保所有 API 端點正常運作
  2. CORS 設定:處理跨域請求問題
  3. 認證準備:建立 token 認證機制
  4. 驗證測試:測試各種邊界情況
  5. 效能基準:確保 API 回應速度
  6. 錯誤處理:測試系統恢復能力

明天我們將把這些準備工作付諸實踐,完成前後端的整合測試!🚀


上一篇
Day 27 - E2E 測試預覽 🎬
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言