「為什麼我的測試總是跑不過?明明程式碼都沒問題啊!」經過一番調查,發現是因為他的測試環境設置不一致。這讓我想到,如果能在測試的關鍵時刻自動執行一些設置或清理工作,是不是就能避免這種問題了?
今天我們要學習 Laravel Pest 的測試生命週期 Hook 功能,確保測試環境的一致性!
基礎篇 Kata 篇 框架篇
Day 1-10 Day 11-17 Day 18-24 ← 我們在這裡!
[=====] [=====] [=====>]
測試生命週期管理
├── 🔄 beforeEach/afterEach
├── 📦 beforeAll/afterAll
├── ⚡ 條件式 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
<?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');
});
// 建立 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,思考測試要點:清空測試快取、記錄快取使用、警告快取未命中、確保測試隔離。
今天我們學習了如何管理測試生命週期:
✅ 資料庫管理、API 認證、效能監控、資源清理
記住:好的生命週期管理讓測試更穩定可靠! 💪