「測試都通過了,為什麼上線還是出問題?」
你寫了完美的單元測試,整合測試也都綠燈,但使用者還是回報:「我點了按鈕,什麼事都沒發生!」這時你才發現,原來是路由設定寫錯了一個參數...
這就是為什麼我們需要 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 測試雖然強大,但執行時間較長。記得合理安排測試策略,確保關鍵流程都有保護!