「測試都通過了,為什麼上線還是出問題?」
你寫了完美的單元測試,整合測試也都綠燈,但使用者還是回報:「我點了按鈕,什麼事都沒發生!」這時你才發現,原來是路由設定寫錯了一個參數...
這就是為什麼我們需要 E2E(End-to-End)測試!今天,讓我們預覽這個強大的測試武器。
基礎測試 → Kata 實戰 → 框架特色 → 整合部署
  1-10        11-17       18-27       28-30
                            ↓ 我們在這裡(Day 27)🎬
[=============================================>....]  
經過 26 天的學習,我們已經建立了完整的測試金字塔。今天要站在金字塔頂端,俯瞰整個測試版圖!
想像一下,你是一個真實的使用者:
E2E 測試就是模擬這整個過程!
        /\
       /E2E\      ← 今天的主角!
      /------\
     /整合測試\
    /----------\
   /  單元測試   \
  /--------------\
Laravel 提供了 Dusk 套件來進行瀏覽器自動化測試:
composer require laravel/dusk --dev
php artisan dusk:install
<?php
namespace Tests\Browser;
use App\Models\User;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class TodoE2ETest extends DuskTestCase
{
    protected User $user;
    protected function setUp(): void
    {
        parent::setUp();
        $this->user = User::factory()->create();
    }
    /** @test */
    public function userCanCompleteTodoWorkflow()
    {
        $this->browse(function (Browser $browser) {
            $browser->loginAs($this->user)
                    ->visit('/todos')
                    ->assertSee('My Todo List')
                    ->type('input[name=title]', '完成 E2E 測試')
                    ->press('Add Todo')
                    ->waitForText('完成 E2E 測試')
                    ->click('.todo-checkbox')
                    ->waitFor('.completed')
                    ->click('.delete-btn')
                    ->waitUntilMissing('.todo-item')
                    ->assertDontSee('完成 E2E 測試');
        });
    }
}
這個測試完整模擬了:登入 → 新增 → 完成 → 刪除
// API 層級測試
test('canCreateTodoViaApi', function () {
    $response = $this->postJson('/api/todos', [
        'title' => '新增待辦'
    ]);
    
    $response->assertStatus(201);
});
// 完整用戶體驗測試
public function userCanCreateTodoThroughUi()
{
    $this->browse(function (Browser $browser) {
        $browser->visit('/todos')
                ->type('#todo-input', '新增待辦')
                ->press('Add')
                ->assertSee('新增待辦');
    });
}
差異:API 測試驗證後端邏輯,E2E 測試驗證完整用戶體驗。
<?php
namespace Tests\Browser;
use App\Models\Product;
use App\Models\User;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ShoppingFlowTest extends DuskTestCase
{
    protected User $user;
    protected Product $product;
    protected function setUp(): void
    {
        parent::setUp();
        
        $this->user = User::factory()->create([
            'email' => 'test@example.com'
        ]);
        
        $this->product = Product::factory()->create([
            'name' => 'Laravel 測試指南',
            'price' => 299
        ]);
    }
    /** @test */
    public function completeShoppingFlow()
    {
        $this->browse(function (Browser $browser) {
            $browser
                // 瀏覽商品
                ->visit('/')
                ->click("@product-{$this->product->id}")
                ->assertSee($this->product->name)
                
                // 加入購物車
                ->press('加入購物車')
                ->waitForText('已加入購物車')
                
                // 結帳流程
                ->click('@cart-icon')
                ->press('前往結帳')
                ->type('email', 'test@example.com')
                ->type('password', 'password')
                ->press('登入')
                ->waitForLocation('/checkout')
                
                // 填寫配送
                ->type('address', '台北市測試路 123 號')
                ->press('確認訂單')
                ->waitForText('訂單完成');
        });
    }
}
test('newUserRegistrationFlow', function () {
    $this->browse(function (Browser $browser) {
        $browser->visit('/register')
                ->type('name', 'John Doe')
                ->type('email', 'john@example.com')
                ->type('password', 'password')
                ->type('password_confirmation', 'password')
                ->check('terms')
                ->press('註冊')
                ->waitForLocation('/dashboard')
                ->assertAuthenticated();
    });
});
建立可重用的頁面物件:
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Page;
class TodoPage extends Page
{
    public function url()
    {
        return '/todos';
    }
    
    public function elements()
    {
        return [
            '@add-input' => 'input[name=title]',
            '@add-btn' => 'button.add-todo',
            '@todo-list' => '.todo-list',
        ];
    }
    
    public function addTodo(Browser $browser, $title)
    {
        $browser->type('@add-input', $title)
                ->press('@add-btn')
                ->waitForText($title);
    }
}
使用頁面物件:
test('manageTodosUsingPageObject', function () {
    $this->browse(function (Browser $browser) {
        $browser->visit(new TodoPage)
                ->addTodo('使用頁面物件')
                ->assertSeeIn('@todo-list', '使用頁面物件');
    });
});
// 等待元素出現
$browser->waitFor('.modal');
// 等待文字
$browser->waitForText('載入完成');
// 等待消失
$browser->waitUntilMissing('.loading');
// 自定義等待
$browser->waitUsing(10, 1, function () use ($browser) {
    return $browser->element('.result')->getText() === '成功';
});
// ❌ 錯誤:依賴固定等待時間
test('bad practice', function () {
    $this->browse(function (Browser $browser) {
        $browser->click('button')
                ->pause(1000)  // 避免使用
                ->assertVisible('.result');
    });
});
// ✅ 正確:等待特定條件  
test('good practice', function () {
    $this->browse(function (Browser $browser) {
        $browser->click('button')
                ->waitFor('.result')
                ->assertVisible('.result');
    });
});
今天我們預覽了 E2E 測試的強大功能:
單元測試 → 快速回饋、大量覆蓋
整合測試 → 模組協作、API 測試
E2E 測試 → 使用者視角、關鍵流程
小提醒:E2E 測試雖然強大,但執行時間較長。記得合理安排測試策略,確保關鍵流程都有保護!