iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0

「測試都通過了,為什麼上線還是出問題?」

你寫了完美的單元測試,整合測試也都綠燈,但使用者還是回報:「我點了按鈕,什麼事都沒發生!」這時你才發現,原來是 API 的 URL 寫錯了一個字母...

這就是為什麼我們需要 E2E(End-to-End)測試!今天,讓我們預覽這個強大的測試武器。

🗺️ 我們的 TDD 旅程

基礎測試 → Kata 實戰 → 框架特色 → 整合部署
  1-10        11-17       18-27       28-30

                            ↓ 我們在這裡(Day 27)🎬
[=============================================>....]

經過 26 天的學習,我們已經建立了完整的測試金字塔。今天要站在金字塔頂端,俯瞰整個測試版圖!

🎭 E2E 測試是什麼?

想像一下,你是一個真實的使用者:

  1. 打開瀏覽器
  2. 輸入網址
  3. 點擊按鈕
  4. 填寫表單
  5. 檢查結果

E2E 測試就是模擬這整個過程!

測試金字塔回顧 🏔️

        /\
       /E2E\      ← 今天的主角!
      /------\
     /整合測試\
    /----------\
   /  單元測試   \
  /--------------\
  • 單元測試:快速、獨立、大量(Day 1-10 學習)
  • 整合測試:模組間互動(Day 25 學習)
  • E2E 測試:完整使用者流程(今天預覽)

🚀 E2E 測試工具選擇

主流工具比較

工具 優點 缺點 適合場景
Cypress 開發體驗佳、除錯容易 只支援 Chromium 中小型專案
Playwright 跨瀏覽器、速度快 學習曲線較陡 大型專案
Selenium 老牌、生態系完整 設定複雜 企業級應用

今天我們選擇 Playwright,因為它:
✅ 支援所有主流瀏覽器、速度快、穩定度高、與 Vitest 整合良好

💻 設定 Playwright

安裝與基本設定

# 安裝 Playwright
npm install -D @playwright/test

# 安裝瀏覽器
npx playwright install

建立設定檔

// 建立 playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

🎯 第一個 E2E 測試

讓我們為 Todo App 寫第一個 E2E 測試:

// 建立 e2e/todo.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Todo App E2E', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/')
  })

  test('complete todo workflow', async ({ page }) => {
    // 1. 檢查頁面標題
    await expect(page).toHaveTitle(/Todo App/)
    
    // 2. 新增待辦事項
    const input = page.locator('input[placeholder="What needs to be done?"]')
    await input.fill('寫 E2E 測試')
    await input.press('Enter')
    
    // 3. 驗證待辦事項顯示
    const todoItem = page.locator('.todo-item').first()
    await expect(todoItem).toContainText('寫 E2E 測試')
    
    // 4. 標記為完成
    await todoItem.locator('input[type="checkbox"]').click()
    await expect(todoItem).toHaveClass(/completed/)
    
    // 5. 刪除待辦事項
    await todoItem.hover()
    await todoItem.locator('button.destroy').click()
    await expect(todoItem).not.toBeVisible()
  })

})

🔍 進階測試技巧

1. 頁面物件模式(Page Object Model)

// 建立 e2e/pages/TodoPage.ts
import { Page, Locator } from '@playwright/test'

export class TodoPage {
  readonly page: Page
  readonly todoInput: Locator
  readonly todoItems: Locator
  readonly filterAll: Locator
  readonly filterActive: Locator
  readonly filterCompleted: Locator

  constructor(page: Page) {
    this.page = page
    this.todoInput = page.locator('input[placeholder="What needs to be done?"]')
    this.todoItems = page.locator('.todo-item')
    this.filterAll = page.locator('a:text("All")')
    this.filterActive = page.locator('a:text("Active")')
    this.filterCompleted = page.locator('a:text("Completed")')
  }

  async goto() {
    await this.page.goto('/')
  }

  async addTodo(text: string) {
    await this.todoInput.fill(text)
    await this.todoInput.press('Enter')
  }

  async toggleTodo(index: number) {
    await this.todoItems.nth(index).locator('input[type="checkbox"]').click()
  }

  async deleteTodo(index: number) {
    const item = this.todoItems.nth(index)
    await item.hover()
    await item.locator('button.destroy').click()
  }

  async filterBy(filter: 'all' | 'active' | 'completed') {
    switch (filter) {
      case 'all':
        await this.filterAll.click()
        break
      case 'active':
        await this.filterActive.click()
        break
      case 'completed':
        await this.filterCompleted.click()
        break
    }
  }
}

2. 使用頁面物件重構測試

// 更新 e2e/todo-with-pom.spec.ts
import { test, expect } from '@playwright/test'
import { TodoPage } from './pages/TodoPage'

test.describe('Todo App with POM', () => {
  let todoPage: TodoPage

  test.beforeEach(async ({ page }) => {
    todoPage = new TodoPage(page)
    await todoPage.goto()
  })

  test('manage todos efficiently', async () => {
    // 新增多個待辦事項
    await todoPage.addTodo('E2E 測試基礎')
    await todoPage.addTodo('進階測試技巧')
    await todoPage.addTodo('部署與監控')
    
    // 驗證數量
    await expect(todoPage.todoItems).toHaveCount(3)
    
    // 標記完成
    await todoPage.toggleTodo(0)
    await todoPage.toggleTodo(1)
    
    // 篩選驗證
    await todoPage.filterBy('completed')
    await expect(todoPage.todoItems).toHaveCount(2)
    
    await todoPage.filterBy('active')
    await expect(todoPage.todoItems).toHaveCount(1)
  })
})

🎪 視覺回歸測試

E2E 測試還可以捕捉視覺問題:

// 建立 e2e/visual.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Visual Regression', () => {
  test('todo app screenshot', async ({ page }) => {
    await page.goto('/')
    
    // 新增一些測試資料
    await page.locator('input[placeholder="What needs to be done?"]').fill('視覺測試')
    await page.keyboard.press('Enter')
    
    // 截圖比較
    await expect(page).toHaveScreenshot('todo-app.png')
  })
})

🚨 常見陷阱與解決方案

1. 測試不穩定(Flaky Tests)

// ❌ 錯誤:依賴固定等待時間
test('bad practice', async ({ page }) => {
  await page.click('button')
  await page.waitForTimeout(1000) // 避免使用
  await expect(page.locator('.result')).toBeVisible()
})

// ✅ 正確:等待特定條件
test('good practice', async ({ page }) => {
  await page.click('button')
  await page.waitForSelector('.result', { state: 'visible' })
  await expect(page.locator('.result')).toBeVisible()
})

🎁 重點回顧

今天我們預覽了 E2E 測試的強大功能:

✅ 學到的技能

  1. E2E 測試概念:理解測試金字塔頂端
  2. Playwright 設定:建立 E2E 測試環境
  3. 實作技巧:Page Object Model
  4. 視覺測試:截圖比較
  5. 效能整合:結合效能監控

🔄 測試策略總結

單元測試 → 快速回饋、大量覆蓋
整合測試 → 模組協作、API 測試
E2E 測試 → 使用者視角、關鍵流程

💡 最佳實踐

  • 只測試關鍵使用者流程
  • 使用 Page Object 模式組織程式碼
  • 避免測試實作細節、保持測試獨立性、設定合理的超時時間

小提醒:E2E 測試雖然強大,但執行時間較長。記得合理安排測試策略,確保關鍵流程都有保護!


上一篇
Day 26 - 效能測試 ⚡
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言