端到端测试对于交付可靠的 Web 应用至关重要。2026 年两个主流框架是 Playwright 和 Cypress。Playwright 由微软支持,使用浏览器原生自动化协议从外部控制 Chromium、Firefox 和 WebKit。Cypress 在浏览器内部运行测试,提供独特的交互式开发者体验。本指南涵盖了你在做出选择前需要评估的所有方面。
TL;DR:需要多浏览器和多语言支持、原生并行执行或移动端测试,选 Playwright。喜欢交互式调试、纯 JS 团队或较平缓学习曲线,选 Cypress。Playwright 完全免费开源。Cypress 本地免费,Cloud 需付费。
核心要点
- Playwright 通过 CDP/协议从外部控制浏览器,原生支持 Chromium、Firefox 和 WebKit。Cypress 在浏览器内运行。
- Playwright 支持 JS/TS、Python、Java 和 C#。Cypress 仅支持 JS/TS。
- Playwright 开箱即用包含原生并行执行和分片。Cypress 并行 CI 需要 Cypress Cloud(付费)。
- Playwright Trace Viewer 提供完整时间线。Cypress Time Travel 让你悬停命令查看 DOM 快照。
- Playwright 免费(Apache 2.0)。Cypress Cloud 团队版从每月 $75 起。
架构比较
Playwright 和 Cypress 最重要的区别在于它们如何与浏览器交互。
Playwright:进程外浏览器控制
Playwright 通过浏览器原生自动化协议通信,测试进程在浏览器外运行,可控制多个上下文和标签页。
┌──────────────────┐ WebSocket/CDP ┌──────────────┐
│ Test Process │ ◄─────────────────► │ Browser │
│ (Node.js) │ │ Chromium │
│ - Test code │ │ Firefox │
│ - Assertions │ │ WebKit │
│ - Fixtures │ │ [Your App] │
└──────────────────┘ └──────────────┘
Tests run OUTSIDE the browser → full lifecycle controlCypress:浏览器内测试运行器
Cypress 注入浏览器与应用一起运行,可直接访问 DOM,但有单标签页限制。
┌───────────────────────────────────────────┐
│ Browser │
│ ┌─────────────┐ ┌────────────────────┐ │
│ │ Cypress │ │ Your App │ │
│ │ Test Runner │ │ (iframe) │ │
│ │ - Commands │ │ Same JS context │ │
│ └─────────────┘ └────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Node.js Proxy (network + filesystem)│ │
│ └─────────────────────────────────────┘ │
└───────────────────────────────────────────┘
Tests run INSIDE the browser → direct DOM access功能比较表
| Feature | Playwright | Cypress |
|---|---|---|
| Browser support | Chromium, Firefox, WebKit | Chrome, Firefox, Edge, WebKit (exp.) |
| Language support | JS/TS, Python, Java, C# | JS/TS only |
| Parallel execution | Native (workers + sharding) | Cypress Cloud (paid) |
| Network interception | Protocol-level (route) | Proxy-level (intercept) |
| Mobile testing | Device emulation (viewport, UA, touch) | Viewport only |
| Visual testing | Built-in (toHaveScreenshot) | Plugin required |
| Component testing | Experimental (React, Vue, Svelte) | Stable (React, Vue, Angular, Svelte) |
| API testing | Built-in (APIRequestContext) | Built-in (cy.request) |
| Trace/debug | Trace Viewer + UI mode | Time Travel + interactive runner |
| Auto-waiting | All actions + assertions | DOM queries + assertions |
| Multi-tab | Native | Not supported |
| iframe | Native (frameLocator) | Plugin required |
| Test generator | Codegen (records actions) | Cypress Studio (limited) |
| License | Apache 2.0 | MIT (core) + proprietary (Cloud) |
安装和设置
Playwright Setup
# Initialize project
npm init playwright@latest
# playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
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'] } },
],
});Cypress Setup
# Install and open wizard
npm install cypress --save-dev
npx cypress open
// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
retries: { runMode: 2, openMode: 0 },
setupNodeEvents(on, config) {},
},
});浏览器支持
Playwright 自带 Chromium、Firefox、WebKit 二进制文件。Cypress 使用本地安装的浏览器,WebKit 为实验性。
语言支持
Playwright 支持 JS/TS、Python、Java、C#。Cypress 仅支持 JS/TS。
# Playwright multi-language install
npm init playwright@latest # JS/TS
pip install playwright # Python
dotnet add package Microsoft.Playwright # C#
# Maven: com.microsoft.playwright # Java测试编写对比
以下是同一登录流程测试在两个框架中的写法。
Playwright Test
import { test, expect } from '@playwright/test';
test('login and see dashboard', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Welcome back' })).toBeVisible();
await expect(page.getByTestId('user-menu')).toContainText('user@example.com');
});Cypress Test
describe('Login Flow', () => {
it('login and see dashboard', () => {
cy.visit('/login');
cy.get('[data-cy="email-input"]').type('user@example.com');
cy.get('[data-cy="password-input"]').type('password123');
cy.get('[data-cy="login-button"]').click();
cy.url().should('include', '/dashboard');
cy.contains('h1', 'Welcome back').should('be.visible');
cy.get('[data-cy="user-menu"]').should('contain', 'user@example.com');
});
});选择器和定位器
Playwright 鼓励面向用户的定位器。Cypress 使用 CSS 选择器配合 cy.get()。
// Playwright locators (best → fallback)
page.getByRole('button', { name: 'Submit' }) // accessible
page.getByLabel('Email') // form fields
page.getByText('Welcome') // visible text
page.getByTestId('submit-btn') // test IDs
page.locator('css=button.primary') // CSS fallback
// Cypress selectors (best → fallback)
cy.get('[data-cy="submit-btn"]') // dedicated test attr
cy.contains('button', 'Submit') // text content
cy.get('#submit-button') // ID selector
cy.findByRole('button', { name: 'Submit' }) // @testing-library plugin自动等待和重试
Playwright 自动等待元素可操作并自动重试断言。Cypress 重试 DOM 查询但操作命令不重试。
// Playwright: auto-waits for actionability
await page.getByRole('button', { name: 'Save' }).click(); // waits visible+stable
await expect(page.getByText('Saved!')).toBeVisible(); // auto-retry
// Cypress: queries retry, actions don't
cy.get('[data-cy="save-btn"]').click(); // retries cy.get()
cy.get('[data-cy="status"]').should('contain', 'Saved!'); // assertion retries
cy.intercept('POST', '/api/save').as('save'); // wait for network
cy.get('[data-cy="save-btn"]').click();
cy.wait('@save');Page Object Model Comparison
Both frameworks benefit from the Page Object pattern to encapsulate page interactions and reduce test duplication.
Playwright Page Object
// pages/login.page.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign In' });
}
async goto() { await this.page.goto('/login'); }
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
await expect(this.page).toHaveURL('/dashboard');
}
}
// Usage in test:
test('login via page object', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@test.com', 'pass123');
});Cypress Page Object
// pages/login.page.ts
export class LoginPage {
get emailInput() { return cy.get('[data-cy="email"]'); }
get passwordInput() { return cy.get('[data-cy="password"]'); }
get submitButton() { return cy.get('[data-cy="login-btn"]'); }
goto() { cy.visit('/login'); }
login(email: string, password: string) {
this.emailInput.type(email);
this.passwordInput.type(password);
this.submitButton.click();
cy.url().should('include', '/dashboard');
}
}
// Usage in test:
const loginPage = new LoginPage();
it('login via page object', () => {
loginPage.goto();
loginPage.login('user@test.com', 'pass123');
});网络拦截和模拟
两者都支持拦截和模拟网络请求。
Playwright Network Mocking
await page.route('/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Alice' }]),
});
});
// Modify real response
await page.route('/api/config', async (route) => {
const response = await route.fetch();
const json = await response.json();
json.featureFlag = true;
await route.fulfill({ response, json });
});
// Block resources
await page.route('**/*.{png,jpg}', route => route.abort());Cypress Network Mocking
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [{ id: 1, name: 'Alice' }],
}).as('getUsers');
// Modify real response
cy.intercept('GET', '/api/config', (req) => {
req.continue((res) => { res.body.featureFlag = true; });
}).as('getConfig');
// Use fixture file
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
cy.visit('/dashboard');
cy.wait('@getUsers');并行执行和分片
Playwright 默认并行运行,支持分片。Cypress 默认串行,并行需要 Cypress Cloud。
// Playwright: native parallel + sharding
export default defineConfig({ fullyParallel: true, workers: 4 });
# npx playwright test --shard=1/3 (machine 1)
# npx playwright test --shard=2/3 (machine 2)
# npx playwright test --shard=3/3 (machine 3)
# Cypress: serial by default, Cloud for parallel
# npx cypress run (serial)
# npx cypress run --record --parallel --key KEY (Cloud required)CI/CD 集成
两者都能很好集成 CI/CD。
Playwright GitHub Actions
name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with: { name: playwright-report, path: playwright-report/ }Cypress GitHub Actions
name: Cypress Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm start
wait-on: http://localhost:3000
- uses: actions/upload-artifact@v4
if: failure()
with: { name: cypress-screenshots, path: cypress/screenshots/ }组件测试
Playwright 实验性支持组件测试。Cypress 组件测试更成熟。
// Cypress Component Test (React)
import { mount } from 'cypress/react18';
import TodoList from './TodoList';
it('adds a new todo', () => {
mount(<TodoList />);
cy.get('[data-cy="todo-input"]').type('Buy groceries{enter}');
cy.get('[data-cy="todo-list"] li').should('have.length', 1);
});视觉测试
Playwright 内置截图比较。Cypress 需要第三方插件。
// Playwright: built-in visual regression
test('homepage visual', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', { maxDiffPixels: 100 });
await expect(page.getByTestId('hero')).toHaveScreenshot('hero.png');
// Mask dynamic content
await expect(page).toHaveScreenshot('page.png', {
mask: [page.getByTestId('timestamp')],
});
});调试体验
Playwright Trace Viewer 重放完整时间线。Cypress Time Travel 悬停命令查看 DOM。
Playwright Trace Viewer
# Record traces in CI (recommended: on-first-retry)
# playwright.config.ts: use: { trace: 'on-first-retry' }
# Record trace for all tests
npx playwright test --trace on
# View trace locally (opens a web app)
npx playwright show-trace trace.zip
# Or upload to trace.playwright.dev for sharing
# Debug with headed browser + Playwright Inspector
npx playwright test --debug
# UI Mode: interactive test development
npx playwright test --ui
# - Watch mode with live reload
# - Click to filter tests
# - Timeline + DOM snapshots + networkCypress Time Travel + DevTools
# Open interactive test runner
npx cypress open
# In the runner:
# - Every command logged in left panel
# - Hover command → DOM snapshot appears
# - Click command → pin and inspect in DevTools
// Programmatic debugging in tests:
cy.get('[data-cy="element"]').then(($el) => {
debugger; // pauses in browser DevTools
});
cy.get('[data-cy="element"]').debug(); // Cypress debug helper
// Log custom messages to command log
cy.log('About to submit the form');
cy.get('[data-cy="submit"]').click();移动端测试
Playwright 提供完整设备模拟。Cypress 仅支持视口调整。
Playwright Mobile Emulation
import { devices } from '@playwright/test';
// Config: test on real device profiles
export default defineConfig({
projects: [
{ name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'iPhone 14', use: { ...devices['iPhone 14'] } },
{ name: 'Pixel 7', use: { ...devices['Pixel 7'] } },
{ name: 'iPad Pro', use: { ...devices['iPad Pro 11'] } },
],
});
// Test with geolocation + permissions
test('mobile map', async ({ page, context }) => {
await context.grantPermissions(['geolocation']);
await context.setGeolocation({ latitude: 37.77, longitude: -122.42 });
await page.goto('/map');
});Cypress Mobile Testing
// Cypress: viewport-based only
describe('Mobile responsive', () => {
it('shows mobile menu', () => {
cy.viewport('iphone-14');
cy.visit('/');
cy.get('[data-cy="desktop-nav"]').should('not.be.visible');
cy.get('[data-cy="mobile-menu-btn"]').should('be.visible').click();
cy.get('[data-cy="mobile-nav"]').should('be.visible');
});
it('tablet layout', () => {
cy.viewport(768, 1024);
cy.visit('/');
});
});性能和速度
| Metric | Playwright | Cypress |
|---|---|---|
| Cold start | ~2-4s | ~5-8s |
| 100 E2E tests (serial) | ~2-4 min | ~5-10 min |
| 100 E2E tests (4x parallel) | ~30s-1min | Requires Cloud |
| Memory usage | Lower (out-of-process) | Higher (in-browser) |
| Network intercept cost | Minimal (protocol) | Moderate (proxy hop) |
测试隔离和 Fixtures
Playwright 使用 fixture 系统。Cypress 使用 Mocha 钩子和 cy.session()。
// Playwright: custom fixtures
const test = base.extend({
authenticatedPage: async ({ browser }, use) => {
const ctx = await browser.newContext({ storageState: 'auth.json' });
const page = await ctx.newPage();
await use(page);
await ctx.close();
},
});
// Cypress: session caching
Cypress.Commands.add('login', (email, pw) => {
cy.session([email, pw], () => {
cy.visit('/login');
cy.get('[data-cy="email"]').type(email);
cy.get('[data-cy="password"]').type(pw);
cy.get('[data-cy="submit"]').click();
cy.url().should('include', '/dashboard');
});
});API 测试
两者都可发送 HTTP 请求进行 API 测试。
Playwright API Testing
import { test, expect } from '@playwright/test';
test('create and verify user', async ({ request }) => {
// Create via API
const response = await request.post('/api/users', {
data: { name: 'Alice', email: 'alice@test.com' },
});
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.name).toBe('Alice');
// Verify via GET
const getRes = await request.get('/api/users/' + user.id);
const fetched = await getRes.json();
expect(fetched.email).toBe('alice@test.com');
});
// API + Browser: set up data then verify in UI
test('API setup + UI verify', async ({ request, page }) => {
await request.post('/api/todos', { data: { title: 'Buy milk' } });
await page.goto('/todos');
await expect(page.getByText('Buy milk')).toBeVisible();
});Cypress API Testing
describe('API: Users', () => {
it('creates and verifies a user', () => {
cy.request('POST', '/api/users', {
name: 'Alice', email: 'alice@test.com',
}).then((res) => {
expect(res.status).to.eq(201);
expect(res.body.name).to.eq('Alice');
// Verify via GET
cy.request('/api/users/' + res.body.id)
.its('body.email')
.should('eq', 'alice@test.com');
});
});
// Setup via API, verify in UI
it('API setup + UI verify', () => {
cy.request('POST', '/api/todos', { title: 'Buy milk' });
cy.visit('/todos');
cy.contains('Buy milk').should('be.visible');
});
});定价比较
| Tier | Price | Includes |
|---|---|---|
| Playwright (all) | Free (Apache 2.0) | All browsers, parallel, sharding, tracing, visual, API, codegen |
| Cypress (open source) | Free (MIT) | Local testing, all browsers, component testing, time travel |
| Cypress Cloud Free | Free | 3 users, 500 results/month |
| Cypress Cloud Team | $75/month | 5 users, unlimited results, parallel, flake detection |
| Cypress Cloud Business | $200/month | 10 users, SSO, priority support |
| Cypress Cloud Enterprise | Custom | Unlimited users, SLA, on-premise |
社区和生态系统
| Metric | Playwright | Cypress |
|---|---|---|
| GitHub stars | ~70K+ | ~48K+ |
| npm weekly downloads | ~8M+ | ~5M+ |
| First release | 2020 | 2017 |
| Backed by | Microsoft | Cypress.io (venture-backed) |
| Stack Overflow | ~25K+ questions | ~35K+ questions |
| Plugin ecosystem | Growing (fewer needed) | Mature (large registry) |
何时选择各框架
Choose Playwright When:
- 需要跨浏览器测试
- 团队使用 Python/Java/C#
- 无付费的原生并行
- 移动端设备模拟
- 内置视觉回归测试
- 多标签/多源场景
- 免费开源方案
Choose Cypress When:
- JS/TS 专注团队
- 重视交互式调试
- 成熟组件测试
- 初级开发者友好
- 主要测试 Chrome
- 托管并行服务
- 丰富插件生态
- Time Travel 调试
迁移指南
两个框架可共存,逐文件迁移。
// Cypress → Playwright cheat sheet
cy.visit('/page') → await page.goto('/page')
cy.get('[data-cy="x"]') → page.getByTestId('x')
cy.contains('Submit') → page.getByText('Submit')
cy.get('input').type('hi') → await locator.fill('hi')
.should('be.visible') → await expect(loc).toBeVisible()
.should('have.text', 'X') → await expect(loc).toHaveText('X')
cy.intercept(method, url) → await page.route(url, handler)
cy.wait('@alias') → await page.waitForResponse(url)
beforeEach(() => {...}) → test.beforeEach(async ({page}) => {...})
// Playwright → Cypress: reverse the above, remove async/await
// Cypress commands are automatically queued最佳实践
Playwright
- 使用面向用户的定位器
- 使用 web-first 断言
- 创建页面对象模型
- 用 fixtures 做认证
- CI 中启用 tracing
- 分片并行
- projects 多浏览器
Cypress
- 用 data-cy 属性
- 避免任意 cy.wait(ms)
- 用 cy.intercept()
- 用 cy.session() 缓存登录
- beforeEach 保持独立
- 自定义命令封装
- 设置 baseUrl
生产环境 CI 配置
Playwright: Sharded Multi-Browser CI
name: Playwright E2E
on: { push: { branches: [main] }, pull_request: { branches: [main] } }
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix: { shard: [1/4, 2/4, 3/4, 4/4] }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci && npx playwright install --with-deps
- run: npx playwright test --shard=\${{ matrix.shard }}
- uses: actions/upload-artifact@v4
if: always()
with: { name: report-\${{ strategy.job-index }}, path: playwright-report/, retention-days: 7 }Cypress: Parallel CI with Cloud
name: Cypress E2E
on: { push: { branches: [main] }, pull_request: { branches: [main] } }
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix: { containers: [1, 2, 3, 4] }
steps:
- uses: actions/checkout@v4
- uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm start
wait-on: http://localhost:3000
record: true
parallel: true
group: e2e-tests
env:
CYPRESS_RECORD_KEY: \${{ secrets.CYPRESS_RECORD_KEY }}
- uses: actions/upload-artifact@v4
if: failure()
with: { name: cypress-artifacts, path: cypress/screenshots/ }常见问题
Playwright 比 Cypress 快吗?
通常是的,快 2-4 倍。但简单测试套件差异缩小。
Cypress 能测试 Safari 吗?
实验性支持 WebKit,但不如 Playwright 成熟。
哪个文档更好?
两者都优秀。Cypress 新手友好,Playwright 全面系统。
可以同时用两者吗?
可以。用 Cypress 做组件测试,Playwright 做跨浏览器 E2E。
CI 必须用 Cypress Cloud 吗?
不需要,Cloud 仅用于并行和分析。
Playwright 支持 iframe 吗?
是的,通过 frameLocator() 一等支持。
哪个对初学者更友好?
Cypress 更适合初学者,有交互式运行器和直观语法。
可以增量迁移吗?
可以,逐文件迁移,迁移期间两者共存。