E2E 測試現在選什麼:Playwright 與 Cypress 的實際差異
對照幾年前用 Cypress 的做法,說明 Playwright 為什麼變成多數新專案的預設選擇:真正的多瀏覽器支援、免費的平行執行、trace viewer 除錯,以及兩者在架構上的根本差異與各自仍然適合的場景。
W
- 幾年前做端對端測試時,Cypress 是很常見的選擇,互動式 Test Runner、
npx cypress open與鏈式 API 都很適合入門。 - 這幾年 Playwright 起來得很快,現在多數新專案會直接選它。
- 這篇講兩者的實際差異,以及什麼情況下 Cypress 還是好選擇——這不是「Cypress 已死」的文章,它仍然是個好工具。
架構上的根本差異
兩者最大的不同在測試程式跑在哪裡。
Cypress 把測試程式跑在瀏覽器裡,跟被測試的應用共用同一個執行環境。這帶來很好的除錯體驗(可以直接 debugger、在 DevTools 裡看),但也帶來限制:
- 難以跨分頁、跨網域操作
- 沒辦法直接碰檔案系統或發原生的 HTTP 請求(要透過
cy.task跳到 Node 端) - 受同源政策約束
Playwright 把測試程式跑在 Node 裡,透過 CDP(Chrome DevTools Protocol)之類的協定遠端驅動瀏覽器。 測試程式與應用完全分離,所以:
- 多分頁、多網域、多使用者情境都是原生支援
- 可以隨意讀寫檔案、呼叫 API、操作資料庫來準備測試資料
- 一份測試可以同時跑在 Chromium、Firefox、WebKit
這個架構差異解釋了後面幾乎所有的功能差別。
主要差異
| 項目 | Cypress | Playwright |
|---|---|---|
| 測試程式執行位置 | 瀏覽器內 | Node 端 |
| 瀏覽器引擎 | Chromium、Firefox、Electron | Chromium、Firefox、WebKit |
| 平行執行 | 需付費的 Cypress Cloud | 內建免費 |
| 多分頁 / 多網域 | 受限 | 原生支援 |
| 語言 | JavaScript / TypeScript | TS/JS、Python、Java、.NET |
| 自動等待 | 有 | 有 |
| 除錯工具 | 互動式 Test Runner | Trace Viewer、UI Mode、Codegen |
| 授權 | MIT(部分功能需付費雲端) | MIT,全部功能免費 |
Playwright 的三個關鍵優勢
1. 真正的 WebKit 測試
Cypress 沒有 WebKit 支援(曾有實驗性支援但不完整)。這代表 Safari 專屬的 bug 測不到——而那正是最常出問題的地方。
Playwright 的 webkit 用的是真正的 WebKit 引擎。雖然仍不完全等於 iOS 上的 Safari(沒有 iOS 的系統整合),但比完全測不到好太多。
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
同一份測試自動在三個引擎上跑。
2. 平行執行免費
Cypress 的平行執行需要 Cypress Cloud(付費)。Playwright 的 workers 是內建的:
export default defineConfig({
workers: process.env.CI ? 4 : undefined,
});
E2E 測試最大的痛點就是慢,一套跑 20 分鐘的測試沒有人願意在每次 PR 都等。平行執行是不是要另外付費,實務上影響很大。
3. Trace Viewer
這是我覺得最有價值的功能。測試失敗時,Playwright 可以錄下完整的執行軌跡:
export default defineConfig({
use: {
trace: 'on-first-retry', // 第一次重試時錄
},
});
失敗後用 npx playwright show-trace trace.zip 打開,會看到:
- 每一步操作的畫面快照,可以來回拖曳
- 每一步的 DOM 狀態(可以用 DevTools 檢查)
- 網路請求、Console log
- 完整的操作時間軸
CI 上偶發失敗的測試最難查,因為你重跑一次就好了。有 trace 之後,那次失敗當下的完整現場都留著。這件事 Cypress 的影片錄製做不到同樣的深度。
寫法對照
概念幾乎一樣,語法有些差異。
Cypress:
describe('登入', () => {
it('用正確帳密可以登入', () => {
cy.visit('/login');
cy.get('[data-testid=email]').type('user@example.com');
cy.get('[data-testid=password]').type('password');
cy.get('button[type=submit]').click();
cy.url().should('include', '/dashboard');
});
});
Playwright:
import { test, expect } from '@playwright/test';
test('用正確帳密可以登入', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('密碼').fill('password');
await page.getByRole('button', { name: '登入' }).click();
await expect(page).toHaveURL(/dashboard/);
});
最明顯的差別是 Playwright 用 await,而 Cypress 用鏈式的隱含佇列。Cypress 的寫法看起來比較短,但那個「看起來像同步、其實不是」的特性也是新手最常困惑的地方——在 Cypress 裡寫 const el = cy.get(...) 拿不到元素。
建議用語意化的定位器
getByRole、getByLabel 這類定位器比 CSS selector 好,理由是它們同時在測無障礙:如果 getByRole('button', { name: '登入' }) 找不到東西,通常代表那顆按鈕對螢幕閱讀器來說也是有問題的。
Cypress 也支援 Testing Library 的同類定位器,這點兩邊都做得到。
從 Cypress 遷移
有幾件事要有心理準備:
cy.intercept對應page.route,API 略有不同- 自訂 command 要改寫成 fixture,Playwright 的擴充機制是 fixture 而不是掛在
cy上 - Cypress 的隱含等待與重試邏輯要改成
expect的 web-first assertion:await expect(locator).toBeVisible()會自動重試,這是 Playwright 處理非同步的核心機制
不建議一次全部搬。實務上的做法是新測試用 Playwright 寫,舊的 Cypress 測試繼續跑,等哪天舊測試需要大改時再順手轉。
Cypress 什麼時候還是好選擇
講完 Playwright 的優勢,也要講清楚 Cypress 仍然有優勢的地方:
- 既有投資:已經有幾百個 Cypress 測試而且跑得好好的,沒有理由為了換而換。
- 互動式除錯體驗:Cypress 的 Test Runner 讓你在測試暫停的狀態下直接操作頁面,這個體驗仍然很好。
- Component Testing:兩邊都有,但 Cypress 的元件測試整合度較成熟。
- 團隊熟悉度:工具再好,團隊不會用就是負擔。
該用哪一個
- 新專案 → Playwright。免費的平行執行、WebKit 支援與 trace viewer 是實打實的差距。
- 既有 Cypress 專案跑得好 → 不用急著換。
- 需要測 Safari 行為 → Playwright,這點沒有替代方案。
- 需要多分頁、多使用者情境(聊天室、協作編輯) → Playwright。
- 主要做元件測試 → 兩邊都行,Cypress 略成熟。