iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0

你有沒有遇過這種情況? 🤔

「為什麼我的測試總是跑不過?明明程式碼都沒問題啊!」經過一番調查,發現是因為他的測試環境設置不一致。這讓我想到,如果能在測試的關鍵時刻自動執行一些設置或清理工作,是不是就能避免這種問題了?

今天我們要學習 Laravel Pest 的測試生命週期 Hook 功能,確保測試環境的一致性!

本日學習地圖 🗺️

基礎篇          Kata 篇           框架篇
Day 1-10        Day 11-17        Day 18-24 ← 我們在這裡!
[=====]         [=====]          [=====>]

測試生命週期管理
├── 🔄 beforeEach/afterEach
├── 📦 beforeAll/afterAll  
├── ⚡ 條件式 Hook
└── 🎯 全域 Hook 設定

為什麼需要測試生命週期 Hook? 🎯

測試生命週期 Hook 是在測試執行的特定時機點自動觸發的程式碼。它們幫助我們管理測試環境,確保每個測試都在相同的條件下執行。

測試挑戰

  • 環境不一致 - 測試間相互影響
  • 重複設置 - 每個測試都要寫相同程式碼
  • 資源清理 - 忘記清理導致測試失敗

使用生命週期 Hook 📦

Laravel Pest 提供了生命週期 Hook 來管理測試環境。

// 建立 tests/Feature/Day24/BasicHookTest.php
<?php

use Tests\TestCase;
use Illuminate\Support\Facades\Log;

beforeEach(function () {
    // 每個測試前執行
    $this->startTime = microtime(true);
    Log::info('Starting test: ' . $this->getName());
});

afterEach(function () {
    // 每個測試後執行
    $duration = microtime(true) - $this->startTime;
    Log::info('Test completed in: ' . round($duration, 3) . 's');
});

beforeAll(function () {
    // 整個測試檔案開始前執行一次
    Log::info('Starting test suite');
});

afterAll(function () {
    // 整個測試檔案結束後執行一次  
    Log::info('Test suite completed');
});

test('hook_execution_order', function () {
    expect(true)->toBeTrue();
});

管理測試資料庫 🔄

讓我們使用 Hook 來管理測試資料庫。

// 建立 tests/Hooks/DatabaseHook.php
<?php

namespace Tests\Hooks;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class DatabaseHook
{
    public static function setupTestDatabase(): void
    {
        // 建立測試用的資料表
        if (!Schema::hasTable('test_users')) {
            Schema::create('test_users', function ($table) {
                $table->id();
                $table->string('name');
                $table->string('email')->unique();
                $table->timestamps();
            });
        }
    }

    public static function seedTestData(): void
    {
        DB::table('test_users')->insert([
            ['name' => 'User 1', 'email' => 'test1@example.com'],
            ['name' => 'User 2', 'email' => 'test2@example.com'],
        ]);
    }

    public static function cleanupTestData(): void
    {
        DB::table('test_users')->truncate();
    }
}

建立 tests/Feature/Day24/DatabaseHookTest.php

// 建立 tests/Feature/Day24/DatabaseHookTest.php
<?php

use Tests\TestCase;
use Tests\Hooks\DatabaseHook;
use Illuminate\Support\Facades\DB;

uses(TestCase::class);

beforeAll(function () {
    DatabaseHook::setupTestDatabase();
});

beforeEach(function () {
    DatabaseHook::seedTestData();
});

afterEach(function () {
    DatabaseHook::cleanupTestData();
});

test('can_query_test_users', function () {
    $users = DB::table('test_users')->get();
    
    expect($users)->toHaveCount(2)
        ->and($users->first()->name)->toBe('User 1');
});

test('each_test_has_fresh_data', function () {
    // 因為 afterEach 清理了資料,這裡又是全新的 2 筆資料
    $users = DB::table('test_users')->get();
    expect($users)->toHaveCount(2);
});

測試條件式設置 ⚡

// 建立 tests/Feature/Day24/ConditionalHookTest.php  
<?php

use Tests\TestCase;

uses(TestCase::class);

beforeEach(function () {
    // 根據測試名稱決定設置
    $testName = $this->getName();
    
    if (str_contains($testName, 'admin')) {
        $this->isAdmin = true;
        $this->permissions = ['create', 'read', 'update', 'delete'];
    } else {
        $this->isAdmin = false;
        $this->permissions = ['read'];
    }
});

test('admin_can_perform_all_actions', function () {
    expect($this->isAdmin)->toBeTrue()
        ->and($this->permissions)->toContain('delete');
});

test('user_has_limited_permissions', function () {
    expect($this->isAdmin)->toBeFalse()
        ->and($this->permissions)->not->toContain('delete');
});

測試 API 認證設置 🔁

// 建立 tests/Hooks/ApiTestHook.php
<?php

namespace Tests\Hooks;

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

class ApiTestHook
{
    private static $testUser;
    private static $authToken;

    public static function setupApiUser(): User
    {
        self::$testUser = User::factory()->create([
            'email' => 'api-test@example.com',
            'name' => 'API Test User',
        ]);
        return self::$testUser;
    }

    public static function authenticateUser(): string
    {
        if (!self::$testUser) {
            self::setupApiUser();
        }
        Sanctum::actingAs(self::$testUser, ['*']);
        self::$authToken = self::$testUser->createToken('test-token')->plainTextToken;
        return self::$authToken;
    }

    public static function cleanupApiUser(): void
    {
        if (self::$testUser) {
            self::$testUser->tokens()->delete();
            self::$testUser->delete();
            self::$testUser = null;
        }
    }
}

全域生命週期設定 🌍

// 更新 tests/Pest.php
<?php

uses(Tests\TestCase::class)->in('Feature', 'Unit');

// 群組特定 Hook
uses()->beforeEach(function () {
    // Feature 測試特定設置
    $this->withoutExceptionHandling();
})->in('Feature');

// 自定義 Helper 在所有測試前執行
beforeEach(function () {
    config(['cache.default' => 'array']);
    config(['session.driver' => 'array']);
});

測試執行時間監控 ⏱️

// 建立 tests/Hooks/PerformanceHook.php
<?php

namespace Tests\Hooks;

use Illuminate\Support\Facades\Log;

class PerformanceHook
{
    private static $metrics = [];

    public static function startTimer(string $testName): void
    {
        self::$metrics[$testName] = [
            'start' => microtime(true),
            'memory_start' => memory_get_usage(true),
        ];
    }

    public static function endTimer(string $testName): void
    {
        if (!isset(self::$metrics[$testName])) {
            return;
        }
        $metrics = &self::$metrics[$testName];
        $metrics['duration'] = microtime(true) - $metrics['start'];
        // 記錄慢測試
        if ($metrics['duration'] > 1.0) {
            Log::warning("Slow test: {$testName}", [
                'duration' => round($metrics['duration'], 3) . 's',
            ]);
        }
    }
}

測試練習題 🏋️‍♂️

試著建立快取管理 Hook,思考測試要點:清空測試快取、記錄快取使用、警告快取未命中、確保測試隔離。

本日重點回顧 📝

今天我們學習了如何管理測試生命週期:

核心概念

  • beforeEach/afterEach - 每個測試的設置與清理
  • beforeAll/afterAll - 測試套件的設置與清理
  • 條件式設置 - 根據測試需求動態設置
  • 全域設定 - 跨測試檔案共享設置

測試技巧

✅ 資料庫管理、API 認證、效能監控、資源清理

延伸思考 💭

  • 如何設計通用的測試設置?
  • 如何避免 Hook 之間的依賴?
  • 如何確保 Hook 的執行效能?

記住:好的生命週期管理讓測試更穩定可靠! 💪


上一篇
Day 23 - 測試篩選與路由 🎯
下一篇
Day 25 - 整合測試 🔗
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言