iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0

昨天我們完成了 Roman Numeral Kata,現在進入框架特定測試的新階段!想像一個場景:專案上線前夕,PM 緊張地問:「API 都測試過了嗎?」你自信地回答:「每個端點都有完整的測試覆蓋!」🎯 這就是今天要學習的 HTTP 測試。

本日學習地圖 🗺️

基礎階段             Kata 階段            框架特定測試
Days 1-10           Days 11-17           Days 18-27
   ✅                  ✅                  📍 Day 18

                                        [HTTP 測試基礎] <- 今天在這
                                              ⬇️
                                        資料庫測試設置
                                              ⬇️
                                          實戰應用開發

為什麼需要 HTTP 測試?🤔

在 Laravel 中,HTTP 測試允許我們:

  • 測試完整的請求生命週期
  • 驗證路由、中間件、控制器的整合
  • 確保 API 回應符合預期
  • 不需啟動真實伺服器

環境準備

建立測試結構

// 建立 tests/Feature/Day18/HttpTestingBasicsTest.php
<?php

namespace Tests\Feature\Day18;

use Tests\TestCase;

test('makes successful GET request', function () {
    $response = $this->get('/api/health');
    
    expect($response->status())->toBe(200);
});

建立基本路由

// 建立 routes/api.php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/health', function () {
    return response()->json(['status' => 'ok']);
});

Laravel HTTP 測試基礎 💡

1. 基本請求方法

// 更新 tests/Feature/Day18/HttpTestingBasicsTest.php
test('supports different HTTP methods', function () {
    // GET 請求
    $response = $this->get('/api/users');
    expect($response->status())->toBe(200);
    
    // POST 請求
    $response = $this->post('/api/users', [
        'name' => 'John Doe',
        'email' => 'john@example.com'
    ]);
    expect($response->status())->toBe(201);
    
    // PUT 請求
    $response = $this->put('/api/users/1', [
        'name' => 'Jane Doe'
    ]);
    expect($response->status())->toBe(200);
    
    // DELETE 請求
    $response = $this->delete('/api/users/1');
    expect($response->status())->toBe(204);
});

2. JSON API 測試

test('sends and receives JSON data', function () {
    // 發送 JSON 請求
    $response = $this->postJson('/api/users', [
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'age' => 30
    ]);
    
    // 驗證狀態碼
    expect($response->status())->toBe(201);
    
    // 驗證 JSON 結構
    $response->assertJsonStructure([
        'data' => ['id', 'name', 'email', 'age', 'created_at']
    ]);
    
    // 驗證 JSON 內容
    $response->assertJson([
        'data' => [
            'name' => 'John Doe',
            'email' => 'john@example.com'
        ]
    ]);
});

3. 驗證回應內容

test('validates response content', function () {
    $response = $this->get('/api/products');
    
    $response->assertOk()
            ->assertSee('Product Name')
            ->assertJsonPath('data.0.name', 'Product 1')
            ->assertHeader('Content-Type', 'application/json');
});

實戰範例:建立簡單的 API 🚀

建立控制器

// 建立 app/Http/Controllers/Day18/TaskController.php
<?php

namespace App\Http\Controllers\Day18;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class TaskController extends Controller
{
    private static $tasks = [];
    
    public function index()
    {
        return response()->json(['data' => self::$tasks]);
    }
    
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'completed' => 'boolean'
        ]);
        
        $task = [
            'id' => count(self::$tasks) + 1,
            'title' => $validated['title'],
            'completed' => $validated['completed'] ?? false,
            'created_at' => now()->toDateTimeString()
        ];
        
        self::$tasks[] = $task;
        return response()->json(['data' => $task], 201);
    }
}

設定路由

// 更新 routes/api.php
use App\Http\Controllers\Day18\TaskController;

Route::prefix('tasks')->group(function () {
    Route::get('/', [TaskController::class, 'index']);
    Route::post('/', [TaskController::class, 'store']);
});

完整測試套件

// 建立 tests/Feature/Day18/TaskApiTest.php
<?php

namespace Tests\Feature\Day18;

use Tests\TestCase;

test('lists all tasks', function () {
    $response = $this->getJson('/api/tasks');
    
    $response->assertOk()
            ->assertJsonStructure(['data']);
});

test('creates new task', function () {
    $taskData = [
        'title' => 'Learn HTTP Testing',
        'completed' => false
    ];
    
    $response = $this->postJson('/api/tasks', $taskData);
    
    $response->assertCreated()
            ->assertJsonPath('data.title', 'Learn HTTP Testing')
            ->assertJsonPath('data.completed', false)
            ->assertJsonStructure([
                'data' => ['id', 'title', 'completed', 'created_at']
            ]);
});

test('validates task creation input', function () {
    $response = $this->postJson('/api/tasks', []);
    $response->assertStatus(422)
            ->assertJsonValidationErrors(['title']);
});

test('retrieves specific task', function () {
    $createResponse = $this->postJson('/api/tasks', ['title' => 'Test Task']);
    $taskId = $createResponse->json('data.id');
    
    $this->getJson("/api/tasks/{$taskId}")
         ->assertOk()
         ->assertJsonPath('data.title', 'Test Task');
});

test('returns 404 for nonexistent task', function () {
    $response = $this->getJson('/api/tasks/999');
    
    $response->assertNotFound()
            ->assertJson(['error' => 'Task not found']);
});

test('updates task', function () {
    $createResponse = $this->postJson('/api/tasks', ['title' => 'Original Title']);
    $taskId = $createResponse->json('data.id');
    
    $this->putJson("/api/tasks/{$taskId}", ['title' => 'Updated Title'])
         ->assertOk()
         ->assertJsonPath('data.title', 'Updated Title');
});

test('deletes task', function () {
    $createResponse = $this->postJson('/api/tasks', ['title' => 'Task to Delete']);
    $taskId = $createResponse->json('data.id');
    
    $this->deleteJson("/api/tasks/{$taskId}")->assertNoContent();
    $this->getJson("/api/tasks/{$taskId}")->assertNotFound();
});

進階技巧 ⚡

1. 使用 Pest 的鏈式語法

// 更新 tests/Feature/Day18/ChainedTest.php
use function Pest\Laravel\{get, post, put, delete};

it('demonstrates chained syntax', function () {
    get('/api/health')
        ->assertOk()
        ->assertJson(['status' => 'ok']);
    
    post('/api/tasks', ['title' => 'New Task'])
        ->assertCreated()
        ->assertJsonPath('data.title', 'New Task');
    
    $response = post('/api/tasks', ['title' => 'Chain Task']);
    $id = $response->json('data.id');
    
    get("/api/tasks/{$id}")->assertOk();
    put("/api/tasks/{$id}", ['completed' => true])->assertOk();
    delete("/api/tasks/{$id}")->assertNoContent();
});

2. 測試認證與檔案上傳 🔒

test('protects routes with authentication', function () {
    $response = $this->getJson('/api/protected');
    $response->assertUnauthorized();
});

test('handles file uploads', function () {
    Storage::fake('uploads');
    $file = UploadedFile::fake()->image('avatar.jpg');
    
    $response = $this->post('/api/upload', ['avatar' => $file]);
    
    $response->assertOk();
    Storage::disk('uploads')->assertExists('avatars/' . $file->hashName());
});

小挑戰 🎯

試著實作這些功能的測試:

  1. 分頁測試:測試 API 的分頁功能
  2. 搜尋測試:測試查詢參數過濾
  3. 排序測試:測試結果排序功能

本日重點回顧 📝

今天我們學習了 Laravel HTTP 測試的基礎:

  1. 基本請求方法:GET、POST、PUT、DELETE
  2. JSON 測試:發送和驗證 JSON 資料
  3. 回應驗證:狀態碼、內容、結構
  4. 完整 CRUD 測試:實作 Task API 測試
  5. Pest 鏈式語法:更優雅的測試寫法

HTTP 測試是 API 開發的基石,讓我們能在不啟動伺服器的情況下,完整測試 API 的行為。這為我們接下來的實戰應用開發打下了堅實的基礎。

明天我們將學習資料庫測試設置,包括測試資料庫的配置、Factory 的使用,以及如何在測試中管理資料庫狀態。準備好深入資料層測試了嗎?明天見!🚀


上一篇
Day 17 - 程式碼整理與回顧 🏁
下一篇
Day 19 - 資料庫測試設置 🗄️
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言