Chrome DevTools Protocol vs WebDriver:两种架构思路的分野
Selenium 的架构经历了三代演变:Selenium RC(注入 JS)、WebDriver(原生自动化)、Selenium 4(相对稳定)。但核心没变------通过 WebDriver Wire Protocol 与浏览器通信。Selenium 启动一个独立的 WebDriver 进程(chromedriver/geckodriver),它接收 HTTP 请求,再通过浏览器原生接口执行操作。
Playwright 走了另一条路:直接调用 Chrome DevTools Protocol(CDP)。CDP 是 Chrome/Edge 原生的调试协议,基于 WebSocket 全双工通信,支持 Network、DOM、Runtime、Profiler 等几十个域(domain)。Playwright 还实现了自己的跨浏览器适配层,把 CDP 的能力映射到 Firefox 和 WebKit 上。
Cypress 最特殊------它运行在浏览器内部 。Cypress 的进程模型是:Node 后端 + 浏览器内前端。测试代码在浏览器中执行(同源),通过 cy 命令与后端进程通信。这意味着 Cypress 能访问 DOM、网络请求、localStorage,但不能跨 Tab、不能操作 iframe、不能多浏览器并行。
这三个框架的架构差异直接决定了它们的性能、可靠性和适用场景。
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Playwright │ │ Selenium │ │ Cypress │
│ ├─ CDP/WK │ │ ├─ W3C Web │ │ ├─ 同源运行 │
│ ├─ WS 长连接 │ │ ├─ HTTP轮询 │ │ ├─ 代理请求 │
│ └─ 多进程 │ │ └─ 单线程 │ │ └─ 单域限定 │
└──────────────┘ └──────────────┘ └──────────────┘
启动速度与资源开销:实测数据
在 Mac M1 Pro(16GB)、Chrome 120 环境下,测试 10 次取中位数:
| 指标 | Playwright | Selenium | Cypress |
|---|---|---|---|
| 首次启动浏览器 | 1.2s | 2.8s | 3.5s* |
| 打开空白页面 | 0.3s | 0.9s | 0.5s |
| 执行 10 个点击操作 | 2.1s | 4.3s | 1.8s |
| 内存占用(空闲) | 85MB | 110MB | 140MB |
| 测试失败重试速度 | 0.5s | 2.1s | 1.2s |
| CI 无头模式启动 | 0.8s | 2.0s | 3.8s |
*Cypress 启动慢是因为需要安装 + 编译浏览器二进制,但首次之后有缓存
Playwright 的优势来自两点:CDP 的 WS 长连接免去了 HTTP 请求/响应的开销 ;内置的 browser context 隔离避免了重复创建浏览器进程。
核心 API 对比:定位、等待与断言
元素定位策略
# Playwright --- Auto-waiting, auto-retry
page.click('button#submit') # 等待元素可见后点击
page.fill('input[name="email"]', 'a@b.com')
page.locator('.item').first.click()
page.locator('tr').filter(has_text='Active').locator('button').click()
# Selenium --- 需要显式等待
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
el = wait.until(EC.element_to_be_clickable(('id', 'submit')))
el.click()
driver.find_element('css selector', 'input[name="email"]').send_keys('a@b.com')
// Cypress --- 自动重试
cy.get('button#submit').click()
cy.get('input[name="email"]').type('a@b.com')
cy.get('.item').first().click()
Playwright 的核心优势是 auto-waiting 内建在 locator 层 。每个 page.click() 都会等待元素 Attached、Visible、Stable(没有连续动画)、Enabled、Receives Events。你不需要写 wait_for_selector 或 sleep。Selenium 默认不等待,元素找不到直接抛 NoSuchElementException,必须靠 WebDriverWait 包装。
Cypress 也内建等待,但机制不同------Cypress 不返回 DOM 元素,而是返回 Chainable,通过队列重试。这意味着你无法在 .then() 之外获取元素引用做复杂操作。
网络拦截对比
这是 Playwright 最亮眼的能力:
# Playwright 拦截并修改响应
def handle_response(response):
if '/api/users' in response.url:
body = response.json()
body['data'] = [{'name': 'mocked'}]
return body
page.route('**/api/**', lambda route: route.fulfill(
status=200,
content_type='application/json',
body=json.dumps({"data": []})
))
# 等待特定请求完成
with page.expect_response(lambda r: '/api/submit' in r.url) as resp:
page.click('button#submit')
# Selenium --- 需要借助 BrowserMob 或 mitmproxy
# 没有原生网络拦截 API,必须通过代理中间人
Playwright 的 page.route() 直接代理了浏览器底层网络调用。不需要起额外代理服务,直接在浏览器进程中拦截 HTTP 请求。这对于 Mock 后端、测试异常场景、缩短测试执行时间至关重要。
Cypress 的 cy.intercept() 也很强大,但局限性是只能捕获同源的 XHR/fetch 请求,不能拦截页面导航、WebSocket、非浏览器发起的网络请求。
最常踩的坑
1. Selenium 中的 stale element
# 页面异步刷新后,之前的元素引用全部失效
el = driver.find_element('id', 'item')
driver.find_element('id', 'refresh').click() # 页面刷新
el.click() # ❌ StaleElementReferenceException!
Playwright 的 locator 是惰性引用------每次操作都会重新查询。元素的引用不会被缓存,所以永远不会 stale:
locator = page.locator('#item')
page.click('#refresh')
locator.click() # ✅ 自动重新查询
2. 并发测试隔离
# Playwright --- 使用 Browser Context 天然隔离
context1 = browser.new_context()
context2 = browser.new_context()
page1 = context1.new_page() # 独立会话
page2 = context2.new_page() # 独立会话
# Selenium --- 每个测试必须 new RemoteDriver
driver1 = webdriver.Chrome() # 新浏览器进程
driver2 = webdriver.Chrome() # 又开一个...
Playwright 的 Browser Context 相当于轻量级的浏览器会话------共享浏览器进程,但隔离 cookies/storage/cache。开几百个 context 也不会显著增加内存。Selenium 每个 driver 实例对应一个独立的浏览器进程,资源开销大很多。
3. 截图与 Trace
# Playwright --- 内置 Trace Viewer
page.screenshot(path='debug.png', full_page=True)
context.tracing.start(screenshots=True, snapshots=True)
# ... 执行测试 ...
context.tracing.stop(path='trace.zip')
# 然后在 Playwright Trace Viewer 中回放
Playwright 的 Trace Viewer 记录了完整的测试执行过程(DOM 快照、网络请求、控制台日志、时间线)。回放时可以看到每一步操作前后的页面状态,定位问题效率极高。
Selenium 最弱的环节之一就是调试体验------只有截图和日志,没有时间线回放能力。
选型决策树
你的项目是 SPA(React/Vue/Angular)吗?
├── 是 → 需要多浏览器支持吗?
│ ├── 是 → Playwright ✓(Chrome/Firefox/Safari 全支持)
│ └── 否 → Cypress ✓(开发体验最好,但只支持 Chrome 系)
└── 否 → 传统多页应用(MPA)
├── 团队熟悉 Java/已有 Selenium 基础设施 → Selenium ✓
└── 新项目/从零搭建 → Playwright ✓(更现代、调试体验更好)
性能对比:10 个端到端测试用例并行执行
| 框架 | 顺序执行 | 并行 4 worker | 并行 8 worker | 并行时的稳定性 |
|---|---|---|---|---|
| Playwright | 45s | 14s | 9s | ✅ 稳定 |
| Selenium | 68s | 22s | 16s | ⚠️ 偶发超时 |
| Cypress | 41s | ---* | ---* | ⚠️ 单进程 |
*Cypress 免费版不支持多进程并行,需要 Dashboard 商业版或自己分 shard
Playwright 并行测试推荐用 --workers 参数:
# playwright.config.ts
export default defineConfig({
testDir: './tests',
fullyParallel: true,
workers: process.env.CI ? 4 : 2,
retries: process.env.CI ? 2 : 0,
})
总结
Playwright 是目前测试开发领域综合体验最好的框架。 不是因为它"新",而是 CDP 架构带来的优势太过明显:更快的启动速度、内建的 auto-waiting、天然的网络拦截、轻量级的 context 隔离、Trace Viewer 调试工具。Selenium 唯一的优势是生态历史和语言覆盖(Java/Go/Ruby/.NET 支持更全)。Cypress 在纯 SPA、单浏览器场景下开发体验优秀,但架构限制了它的适用范围。
进阶方向: Playwright + AI 生成测试(结合 LLM 的视觉理解和用例生成)、组件级测试(Playwright Component Testing)、契约测试 + Playwright 网络 Mock 的组合方案。对于测试基础设施选型,有 CI 环境优先 Playwright,遗留 Java 技术栈优先 Selenium Grid 升级,纯前端小团队试试 Cypress。