从 Cypress 迁移到 Playwright
原文由 Michael Lynch 于 发布,订阅该博客
Cypress 是一个用于 Web 应用端到端测试的开源工具。我在 2018 年纽约的一场 Web 开发聚会上第一次看到 Gleb Bahmutov 演示 Cypress,当时深受震撼。

自从 2018 年在那场开发者聚会上看到演示后,我就一直在用 Cypress。
在接触 Cypress 之前,我一直勉强用着 Selenium。Cypress 带来了耳目一新的飞跃,它优雅地解决了许多让 Selenium 难以实用的痛点。
最近我试用了 Playwright,也就是微软对标 Cypress 的产品。试用了一天后,我已经准备好彻底从 Cypress 转向 Playwright 了。
这么说让我有点难受,因为我对 Cypress 那个小而拼搏的团队很有好感。我当然也不想再多依赖微软这样的巨头,但 Playwright 实在好太多了,让我没法再坚持用 Cypress。
接下来是我从 Cypress 切换到 Playwright 时的一些笔记,趁记忆还新鲜记录下来。
我与 Cypress 和 Playwright 的过往经历
过去四年里,我几乎为自己开发的每一个 Web 应用都写过 Cypress 端到端测试。算是中级 Cypress 用户。我的大部分需求都很基础,只用到基本的 API。我从没写过自定义插件,但用过一些第三方插件。
我只用了一天 Playwright。为了上手,我尝试把其中一个应用的整套测试从 Cypress 移植到 Playwright。我选了 PicoShare,我的极简文件分享工具,它只有 10 个端到端测试。我花了大约 5 个小时就把它们全部从 Cypress 移植到了 Playwright,这其中还包括学习 Playwright API 的时间。
我从未为 Cypress 或 Playwright 付过费,所以也不指望从它们那里得到什么。Cypress 有付费的 SaaS 服务,但我从没购买过,因为不符合我的工作流。我本来很乐意像赞助其他开源项目那样赞助 Cypress,但它并没有提供赞助渠道。
我喜欢 Playwright 的地方
Playwright 比 Cypress 快得多
我的 Playwright 测试套件在 CircleCI 上比等效的 Cypress 测试快 34%。在本地开发机上,Playwright 比 Cypress 快了 5 倍。这不是严格的测量,但两者速度差距明显。
| 任务 | Cypress | Playwright | 差异 |
|---|---|---|---|
| 在 CircleCI 上运行测试 | 127s | 84s | -34% |
| 在本地开发机上运行测试 | 40s | 7s | -83% |
CI 上性能差异的一部分原因在于,Playwright 的 Docker 镜像比 Cypress 的小得多。在本地开发时这无所谓,因为只需下载一次就行。但在 CI 中每次运行 Cypress,都得等 CircleCI 下载并解压约 1GB 的镜像。
| cypress/included:10.9.0 | playwright:v1.26.0-focal-amd64 | |
|---|---|---|
| 大小 | 940 MB | 651 MB |
Playwright 提供了一套一致的断言 API
Cypress 打包了九个不同的第三方库,导致 API 混杂且不一致。有 should、expect 和 assert,具体用哪个关键字取决于上下文。
例如,下面两段代码执行的断言完全相同:
cy.get("#error-message").should("be.visible");cy.get("#error-message").should(($el) => expect($el).to.be.visible);在 Playwright 中,则只有一套一致的 API。要断言 ID 为 error-message 的元素在页面上可见,只需一次简单的函数调用:
expect(page.locator("#error-message")).toBeVisible();Playwright 不依赖图形界面环境
Cypress 最受吹捧的特性之一就是它的桌面 GUI 应用:

Cypress 通过桌面应用展示测试执行过程
Cypress 桌面应用让你可以在测试中“时光回溯”,查看测试每个阶段浏览器窗口的样子。
但如果你在没有图形界面的环境下开发呢?我的所有开发都在无界面的服务器虚拟机上完成。用 Cypress 的四年里,我从没用过它的桌面应用。相反,我是在 Docker 容器里运行 Cypress 的,而这对于一个默认需要在桌面 GUI 中工作的工具来说,有时反而成了障碍。
在 CI 环境中运行 Cypress 测试时,GUI 的问题又会出现。那里通常也没有桌面图形界面。Cypress 的解决方案是使用他们的付费 CI 服务,这也是他们公司主要的盈利方式。
我支持公司以任何方式将开源产品变现,但 Cypress 的 CI 产品从来没有吸引过我。我希望能在本地用 Docker 容器复现 CI 环境。Playwright 可以做到,而 Cypress 的 CI 服务不行。
为了在 CircleCI 上运行 Cypress,我不得不借助 Docker Compose 做一些额外编排。这倒不是多大的负担,但确实让测试栈比我期望的要复杂一些。
而当我尝试 Playwright 时,感觉如沐春风——它就是为无界面运行而设计的。在 CI 中运行 Playwright 完全不需要额外技巧,开箱即用。
Playwright 也有和 Cypress 类似的时光回溯功能,但它是通过 Web UI 而非桌面 GUI 实现的,因此能在更多环境中使用。
时光回溯功能相当不错!Playwright 的快照甚至不只是应用的静态截图。你可以在测试的每个阶段与浏览器交互,感觉有点像魔法。
Playwright 的 Web UI 让你可以回溯到应用执行的不同状态,并与页面上的任意元素交互。
Playwright 的功能短板更少
用 Cypress 做基础的端到端测试很容易上手,但随着应用变大,我经常会碰到测试工具的功能短板。
例如,我加上文件上传功能后,才发现 Cypress 无法测试文件上传。我不得不停下手头工作,去找第三方的 Cypress 插件来补上这个缺口。
就在写这篇文章时,我发现 Cypress 在今年早些时候才原生支持了文件上传,但让人费解的是,支持这样一个极其常见的场景竟然花了七年时间。
同样,如果你想模拟鼠标悬停——几乎每个 Web UI 框架都有的功能——Cypress 却做不到。这个缺陷已经挂了将近八年。
我相信 Playwright 也有自己的短板,但在用一天时间把测试从 Cypress 移植到 Playwright 的过程中,我一个都没碰到。我在测试套件里为弥补 Cypress 缺陷而写的各种变通方案,在 Playwright 中都有原生支持。
Playwright 需要的领域特定知识更少
当初发现 Cypress 时,吸引我的原因之一是它是为 JavaScript 设计的,而 Selenium 则是以 Java 为先。
对于基础测试,Cypress 的语义对懂 JavaScript 的人来说自然而熟悉。但一旦偏离常规用法,Cypress 就不那么像 JavaScript,而更像一个自成一体的领域特定框架。
举个例子,我的应用 PicoShare 有个功能,可以为想分享给未登录用户的文件生成链接。为了测试这个功能,我需要先通过 PicoShare 的分享流程导航,退出登录,然后验证浏览器是否仍能访问几步之前生成的链接。
下面是我最初在 Cypress 中实现这个测试的方式:
// Save the route to the guest link URL so that we can return to it later.
cy.get('.table td[test-data-id="guest-link-label"] a')
.invoke("attr", "href")
.then(($href) => {
// Log out.
cy.get("#navbar-log-out").click();
cy.location("pathname").should("eq", "/");
// Make sure we can still access the guest link after logging out.
cy.visit($href);
// Continue with the test
});你看到 then,可能会以为 invoke 返回了一个 Promise。但如果你尝试 await 这个 Promise,会得到 undefined,因为 Cypress 实际返回的只是一个假装成 Promise 的东西。
这看似不是什么大事,但如果你需要在应用中动态引用某个值,Cypress 会迫使你为每一个需要的值都增加一层嵌套闭包。有一个获得广泛支持的功能请求希望支持 await,但四年里毫无进展,而 Cypress 最近还表示目前没有实现它的计划。
同样的测试在 Playwright 中是这样的:
// Save the route to the guest link URL so that we can return to it later.
const guestLinkRouteValue = await page
.locator('.table td[test-data-id="guest-link-label"] a')
.getAttribute("href");
expect(guestLinkRouteValue).not.toBeNull();
const guestLinkRoute = String(guestLinkRouteValue);
// Log out.
await page.locator("#navbar-log-out").click();
await expect(page).toHaveURL("/");
// Make sure we can still access the guest link after logging out.
await page.goto(guestLinkRoute);
// Continue with the test.在 Playwright 中,当我们拿到 DOM 元素的引用后,可以直接调用 getAttribute 这样的常规 API,得到符合预期的简单返回值,而不必处理闭包的复杂性。而且 Playwright 返回的那些看似 Promise 的值,真的就是可以 await 的 Promise,所以代码更简洁。
在 Playwright 中做文本比较更简单
Cypress 有一点一直让我很头疼,那就是断言某个元素包含特定文本值非常困难。
下面是 PicoShare 中的一个 <p> 元素示例:
<p data-test-id="github-instructions">
Visit our
<a href="https://github.com/mtlynch/picoshare">GitHub repo</a> to create your
own PicoShare server.
</p>Cypress 中不符合预期的文本比较结果
下面是在 Cypress 中断言文本值的直观写法:
cy.get("[data-test-id='github-instructions']").should(
"have.text",
"Visit our GitHub repo to create your own PicoShare server.",
);不幸的是,这个测试会失败:
Timed out retrying after 10000ms
+ expected - actual
-'
Visit our
GitHub repo to create
your own PicoShare server.
'
+'Visit our GitHub repo to create your own PicoShare server.'Cypress 拿到的是 textContent 属性,它包含了原始 HTML 中文本周围的所有空白,而不是文本在浏览器中实际呈现的样子。
你可以通过获取元素的 innerText 来绕过这个问题,但语法很绕、很难记,因为它用的是完全不同的一套断言 API:
cy.get("[data-test-id='github-instructions']").should(($el) => {
expect($el.get(0).innerText).to.eq(
"Visit our GitHub repo to create your own PicoShare server.",
);
});Playwright 中符合预期的文本比较
在 Playwright 中,直观的断言就能得到正确的结果:
await expect(page.locator("data-test-id=github-instructions")).toHaveText(
"Visit our GitHub repo to create your own PicoShare server.",
);Playwright 同样会查看元素的 textContent,但它会自动像浏览器那样去除首尾空白并合并多余空白。
如果你想让 Playwright 改为查看 innerText,语法也比 Cypress 简单得多:
await expect(page.locator("data-test-id=github-instructions")).toHaveText(
"Visit our GitHub repo to create your own PicoShare server.",
{ useInnerText: true },
);Playwright 在这一点上要扣几分,因为它有两个名字相似、看似相同的 API:
toHaveText:“确保Locator指向具有给定文本的元素。你也可以对值使用正则表达式。”toContainText:“确保Locator指向包含给定文本的元素。你也可以对值使用正则表达式。”
一个 API 用于断言元素“具有给定文本”,另一个则用于断言元素“包含给定文本”?“具有”文本和“包含”文本到底有什么区别?
再多读一些文档后,区别似乎在于你对元素子元素的预期有细微不同,但这份文档确实还有改进空间。
Playwright 让操作 Shadow DOM 更容易
我常用 HTML 自定义元素来写 Web 应用,所以代码里经常包含嵌套的 Shadow DOM。
在 Cypress 中,指定 Shadow DOM 内的页面元素有点别扭,因为每遇到一次 Shadow DOM 边界,就得打断一次 CSS 选择器:
cy.get("#upload-result upload-links")
.shadow()
.find("#verbose-link-box")
.shadow()
.find("#link")
.should("be.visible");Playwright 默认会穿透 Shadow DOM,因此 CSS 选择器可以写得很简洁:
await expect(
page.locator("#upload-result upload-links #verbose-link-box #link"),
).toBeVisible();更新(2022-10-26):Reddit 用户 /u/Daffodils2 指出,Cypress 提供了一个 includeShadowDom 选项,可以让它在跨 Shadow DOM 选取元素时的行为与 Playwright 一致。
Playwright 会帮你启动应用
Cypress 一个奇怪的设计决策是,它拒绝帮你启动应用。你得自己想办法把应用跑起来,然后编排 Cypress 测试在应用开始服务后再启动。
Playwright 消除了这种编排上的麻烦,并提供了一个简单的配置项来启动应用。下面是 PicoShare 中的配置示例:
webServer: {
command: "PS_SHARED_SECRET=dummypass PORT=6001 ./bin/picoshare",
port: 6001,
},Playwright 的日志真的能用
Cypress 的一大痛点是,你得学会在没有终端调试日志的情况下工作。Cypress 没有官方的向 stdout 或 stderr 打印的方法。
如果我加一句 console.log,什么也不会发生:
console.log("hello from Cypress"); // this does nothingCypress 有自己的 cy.log API,那试试这个呢?
cy.log("hello from Cypress"); // this prints nothing to the terminal也不行。它只会在 Cypress 桌面 GUI 或其专有的 SaaS 控制面板中打印输出。
Cypress 开发者 Zach Bloomquist 发布了一个非官方插件,用于把浏览器控制台输出打印到终端,但那是第三方插件,并非 Cypress 官方支持。
在 Playwright 中,console.log 直接就能用,简单省心:
console.log("hello from Playwright");运行测试时,我能在终端输出中看到日志信息:
[chromium] › auth.spec.ts:3:1 › logs in and logs out
hello from PlaywrightPlaywright 团队看起来并不缺资源
Cypress 核心仓库有 2782 个未解决的 issue,其中一些是被搁置多年的重要功能请求。有时人们会用插件来填补空缺,但总感觉 Cypress 核心团队根本没有足够的资源跟上现代 Web 开发的步伐。
一年前我给 Cypress 提了一个毫无争议的 PR,到现在他们都还没理会。我猜他们只是没有资源来审查外部的拉取请求。
相比之下,Playwright 只有 603 个未解决的 issue,尽管收到的缺陷报告数量大致相当。当我给 Playwright 提了一个 bug 后,他们在不到一个工作日内就进行了分类并给出了有意义的回复。
Playwright 与 VS Code 的集成更好
Playwright 提供了官方的 VS Code 插件,能提供上下文感知的自动补全。这是我在 Cypress 中从未意识到自己缺少、直到在 Playwright 中看到才发现的功能:

Playwright 的 VS Code 插件提供上下文感知的自动补全。
在 Cypress 中,函数数量很少,你通过传入特殊的字符串值来触发不同功能。IDE 很难对这种语义提供帮助,而 Playwright 一系列明确的 TypeScript 函数让 IDE 更容易为你提供辅助。Cypress 也有第三方的 VS Code 插件,但没有 Cypress 团队官方支持的。
在 Playwright 中并行测试是免费的
理论上,你也可以在 Cypress 中免费运行并行测试,但他们故意把它做得很不方便。这也不能怪他们,因为并行测试是 Cypress 付费 SaaS 工具的旗舰功能之一,把免费版做得更好用会让他们亏钱。
微软的财力远比 Cypress 雄厚,因此可以把 Playwright 的所有功能都免费开放。也正因如此,Playwright 开箱即支持并行测试。
我怀念 Cypress 的地方
Cypress 的语法更连贯流畅
Cypress 和 Playwright 都提供了流式 API,你可以把一系列操作链式串联成一条语句。
Cypress 更严格地遵循流式风格,让开发者可以从左到右阅读测试逻辑。
cy.get(".navbar-item [data-test-id='log-in']").should("be.visible");在 Cypress 中,我写代码的顺序与我思考测试的顺序一致。先拿到元素的引用,然后再考虑要做什么断言。
在 Playwright 中,顺序有点混乱。在开始定位要测试的元素之前,我得先把代码包在 expect 调用里:
await expect(
page.locator(".navbar-item [data-test-id='log-in']"),
).toBeVisible();Playwright 的语法打断了我从 Cypress 习惯的从左到右的顺序。我希望 Playwright 的语法能更像这样:
// INVALID - not how Playwright actually behaves
await page
.locator(".navbar-item [data-test-id='log-in']")
.expect()
.toBeVisible();Cypress 拥有小而独立的团队
我个人很欣赏 Cypress 这家开源公司,尤其是他们的工程副总裁 Gleb Bahmutov。Gleb 发布的博客文章质量很高,演讲也非常出色。
当我写了一篇关于 Cypress 的博客后,Gleb 热心地提供了反馈帮我改进文章。发布之后,Cypress 还在他们的博客上推广了我的文章。
另一方面,微软历史上曾对开源抱有敌意。现在正处于友好期,但如果风向变了,他们发现通过打压开源能赚更多钱,很可能就会那么做。
如果这是一部电影,Cypress 就是那个让人忍不住为之加油的拼搏的小人物,而微软则是那个已经改过自新、但很可能在第三幕背叛主角的反派。
Cypress 的测试产物在 CI 中可用
当 Cypress 测试失败时,它会在失败点截取应用的屏幕截图并保存到磁盘。你可以很方便地配置 CI 平台,将这些图片作为测试产物保留,便于调试。同样,Cypress 还能为每个测试保存视频,你也可以将其作为 CI 测试产物发布。

Cypress 生成的测试产物很容易作为 CI 产物查看
Playwright 生成的测试产物则要复杂一些。它不是简单的图片和视频,而是生成一个用于查看所有测试产物的静态 Web 应用。
遗憾的是,Playwright 的报告查看器在 CircleCI 上无法工作,所以我不得不下载产物并在本地运行 Playwright 服务器,而不能直接在 CircleCI 控制面板中查看。
Cypress 的 Docker 镜像真的包含了软件本身
在一种我只在端到端测试工具中见过的模式里,Cypress 和 Playwright 的官方 Docker 镜像实际上并不包含工具本身。也就是说,Cypress 的 Docker 镜像里没有 Cypress,Playwright 的 Docker 镜像里也没有 Playwright。
相反,这些 Docker 镜像包含的是分别安装 Cypress 或 Playwright 所需的依赖。所以当你运行 Playwright 的 Docker 镜像时,仍需在环境准备阶段再安装一次 Playwright。
这背后一定有某些合理的理由,但我始终没弄明白。当我向 Cypress 团队抱怨这一点时,他们专门新增了一个包含 Cypress 工具本身的 cypress/included 镜像。而 Playwright 似乎并没有等效的 Docker 镜像。
总结
尽管我使用 Playwright 才几个小时,却发现它的体验比 Cypress 好得多。凭借更清晰的 API、更简单的测试配置和更快的速度,我在 Playwright 上的效率比在 Cypress 上高出 50% 到 100%。
往后,我所有的新应用都会用 Playwright 来测试。对于那些测试时间已经超过五分钟的应用,我甚至可能会把一些旧的 Cypress 测试也移植到 Playwright。
如果你是 Cypress 用户,我强烈建议你试试 Playwright。对我而言,从 Cypress 到 Playwright 的飞跃,就像当初从 Selenium 到 Cypress 一样巨大。
随机一篇博客
评论
登录后参与讨论