DevToolBox免费
博客

Playwright vs Cypress 2026:全面对比 — 选择哪个 E2E 测试框架?

27 分钟阅读作者 DevToolBox Team

端到端测试对于交付可靠的 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 control

Cypress:浏览器内测试运行器

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

功能比较表

FeaturePlaywrightCypress
Browser supportChromium, Firefox, WebKitChrome, Firefox, Edge, WebKit (exp.)
Language supportJS/TS, Python, Java, C#JS/TS only
Parallel executionNative (workers + sharding)Cypress Cloud (paid)
Network interceptionProtocol-level (route)Proxy-level (intercept)
Mobile testingDevice emulation (viewport, UA, touch)Viewport only
Visual testingBuilt-in (toHaveScreenshot)Plugin required
Component testingExperimental (React, Vue, Svelte)Stable (React, Vue, Angular, Svelte)
API testingBuilt-in (APIRequestContext)Built-in (cy.request)
Trace/debugTrace Viewer + UI modeTime Travel + interactive runner
Auto-waitingAll actions + assertionsDOM queries + assertions
Multi-tabNativeNot supported
iframeNative (frameLocator)Plugin required
Test generatorCodegen (records actions)Cypress Studio (limited)
LicenseApache 2.0MIT (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 + network

Cypress 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('/');
  });
});

性能和速度

MetricPlaywrightCypress
Cold start~2-4s~5-8s
100 E2E tests (serial)~2-4 min~5-10 min
100 E2E tests (4x parallel)~30s-1minRequires Cloud
Memory usageLower (out-of-process)Higher (in-browser)
Network intercept costMinimal (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');
  });
});

定价比较

TierPriceIncludes
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 FreeFree3 users, 500 results/month
Cypress Cloud Team$75/month5 users, unlimited results, parallel, flake detection
Cypress Cloud Business$200/month10 users, SSO, priority support
Cypress Cloud EnterpriseCustomUnlimited users, SLA, on-premise

社区和生态系统

MetricPlaywrightCypress
GitHub stars~70K+~48K+
npm weekly downloads~8M+~5M+
First release20202017
Backed byMicrosoftCypress.io (venture-backed)
Stack Overflow~25K+ questions~35K+ questions
Plugin ecosystemGrowing (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 更适合初学者,有交互式运行器和直观语法。

可以增量迁移吗?

可以,逐文件迁移,迁移期间两者共存。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON Formatter.*Regex Tester

相关文章

Playwright E2E 测试完全指南

学习 Playwright 端到端测试。

Jest测试指南:模拟、React Testing Library、快照和代码覆盖率

掌握Jest JavaScript测试。涵盖describe/it/expect单元测试、jest.fn()和jest.mock()模拟、React Testing Library、异步测试、快照测试、代码覆盖率以及Jest vs Vitest vs Mocha对比。

Cypress E2E 测试完全指南:端到端测试最佳实践

掌握 Cypress 端到端测试,包括选择器、命令、fixture、拦截、组件测试与 CI 集成。