以下为作者观点:
早在2019年,我就开始使用Cypress ,当时我所在的公司决定在新项目中放弃Protractor 。当时,我使用的框架是Angular,并且有机会实施Cypress PoC。最近,我换了工作,现在正在使用React,在那里我也有机会实现Playwright PoC。
个人感觉,有了Angular和React的经验,我更倾向于使用data-testid属性进行测试。这让我能够在UI端到端测试中保持一致的方法,而且我没有观察到Angular和React应用程序在测试中存在任何显著差异。
注:在本文中,我使用了一个React应用程序作为Cypress和Playwright的示例。
这两个测试框架都提供什么?
Cypress和Playwright都能够提供出色的UI测试体验(还可以测试API)。它们的自动等待功能让开发人员更容易编写测试,在测试运行时提供可视化用户界面,还能生成测试截图和视频,并支持类型脚本。
这两个框架都支持可视化组件测试,但我不会在本文中讨论这个主题。
由于这两个框架提供的功能非常相似,因此我将剖析每个框架是如何实现以下方面的,以及它们如何影响开发人员体验/工作效率的:
编写测试所用的语法对学习曲线和每个框架的整体易用性起着至关重要的作用。包括但不限于使用自定义命令扩展框架和记录测试。
测试的执行和可维护性是影响开发人员对测试的信心和调试时间的重要因素,特别是在速度和稳定性方面。
测试报告在测试过程中发挥着重要作用,评估其设置的难易程度及其提供的信息水平对于这两个框架都至关重要。
我的Cypress使用体验
安装Cypress非常简单,只需依赖NPM就可以了。没过多久就遇到了使用 Cypress 时需要了解的复杂细节。以下是一些主要细节:
● 用于编写测试的语法
Cypress使用类似Promise的语法来编写测试。乍一看这似乎并不令人困惑,但开发人员倾向于认为,因为他们的API看起来像promise(承诺),所以其行为也像promise(async/await,异步/等待)。可悲的现实是,事实并非如此,这导致开发人员花费大量时间学习如何使用Cypress API,并使其适合他们特定的测试场景。
如果需要使用async/await,你有两种选择:要么将其封装在Cypress命令中,以便添加到Cypress命令链中;要么使用cypress-promise这样的库。下面是每个选项的示例:
// async-await.spec.ts
import promisify from 'cypress-promise';
function sleep(milliseconds: number) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function asyncFunction(text: string) {
console.log('started asyncFunction ' + text);
await sleep(3000);
console.log('finalized asyncFunction ' + text);
}
context('Async/Await Test', () => {
beforeEach(() => {
cy.visit('/');
});
it('convert promises into cypress commands, do not write tests using async/await', () => {
cy.wrap(null).then(() => asyncFunction('first'));
cy.wrap(null).then(() => asyncFunction('second'));
});
it('convert cypress commands to promises, should be able to code with async/await', async () => {
const foo = await promisify(cy.wrap('foo'));
const bar = await promisify(cy.wrap('bar'));
expect(foo).to.equal('foo');
expect(bar).to.equal('bar');
});
});
● 测试报告
Cypress缺乏内置的测试报告功能,这意味着获取所有测试、测试持续时间以及故障截图和链接视频等附加元素的全面总结,可能是一个具有挑战性的繁琐过程。尽管在配置报告方面投入了大量精力,但在生成的报告中仍可能会出现缺少屏幕截图的情况。
测试失败的基本 Cypress Mocha 测试报告
// package.json: example of cumbersome test reporting setup
"scripts": {
"-------------------- E2E Commands --------------------": "",
"cypress:open": "nyc cypress open",
"cypress:run": "npm-run-all -s --continue-on-error _clean _cypress-run:run _cypress-run:html",
"-------------------- Supporting Commands --------------------": "",
"_clean": "npx rimraf coverage cypress/output/junit cypress/output/mocha-json cypress/output/mocha-html/*.html cypress/output/mocha-html/*.json .nyc_output",
"_coverage-report": "npx nyc report --reporter html",
"_cypress-run:run": "nyc cypress run --headless --browser chrome",
"_cypress-run:html:merge-json": "mochawesome-merge cypress/output/mocha-json/*.json > cypress/output/mocha-html/merged-mochawesome.json",
"_cypress-run:html:gen-html-from-json": "marge cypress/output/mocha-html/merged-mochawesome.json -f cypress -o cypress/output/mocha-html -i true --charts true",
"_cypress-run:html": "npm-run-all -s _cypress-run:html:merge-json _cypress-run:html:gen-html-from-json _coverage-report"
},
● 如何运行head测试
Cypress会在其自己的浏览器应用程序中运行你的网络应用程序。它首先会打开一个页面,列出你的所有测试specs,然后你可以单击某个规范来运行该规范内的所有测试。我想补充的是,所有这一切都非常缓慢。此外,如果你只想在spec中运行一个测试,则需要将其标记为only,保存后只有该测试会自动重新加载。
在Cypress浏览器内运行的应用程序
● 如何在headless运行中访问浏览器控制台日志
浏览器控制台日志只有在以标题形式(打开DevTools)运行测试时才可见。这意味着,如果你的测试在本地成功运行,但在CI管道上失败了,那么你要想把这些日志传到管道控制台就会很麻烦。幸运的是,有一个插件可以解决这个问题,但由于Cypress插件的工作方式,这不仅仅是一个即插即用的工作。
只有在打开DevTools时才能看到浏览器控制台日志
// cypress/plugins/index.ts
interface Browsers {
family: string;
name: string;
}
interface LaunchOptions {
args: string[];
}
const cypressPLugins = (on: unknown, config: unknown) => {
require('@cypress/code-coverage/task')(on, config);
// log console.* messages to the cypress console,
// it helps when there are errors on the CI/CD pipeline
require('cypress-log-to-output').install(on, consoleToLogConfig);
require('@cypress/react/plugins/react-scripts')(on, config);
// @ts-ignore
on('before:browser:launch', (browser: Browsers, launchOptions: LaunchOptions) => {
if (browser.family === 'chrome' || browser.name === 'chrome') {
console.log('Adding chrome config...');
launchOptions.args.push('--disable-dev-shm-usage');
launchOptions.args.push('--lang=en');
}
return launchOptions;
});
return config;
};
export const consoleToLogConfig = (_type: unknown, event: { level: string; type: string }) => {
// return true or false from this plugin to control if the event is logged on the cypress console
// `type` is either `console` or `browser`
// if `type` is `browser`, `event` is an object of the type `LogEntry`:
// https://chromedevtools.github.io/devtools-protocol/tot/Log/#type-LogEntry
// if `type` is `console`, `event` is an object of the type passed to `Runtime.consoleAPICalled`:
// https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#event-consoleAPICalled
// only show error events:
return event.level === 'error' || event.type === 'error';
};
export default cypressPLugins;
● 调试测试(Debugging Tests)
Cypress中有几种不同的调试选项,由于Cypress命令是异步运行的,因此不能直接添加调试器(debugger)命令。
本质上,Cypress提供了2个有用的命令:debug()和pause()。前一个命令会向Cypress运行程序添加调试器命令,后一个命令则会在此时停止测试运行。两者都提供了检查DOM的能力,但只有使用 pause() 命令,你才能逐步检查测试中即将执行的每个Cypress 命令。
鉴于Cypress使用类似Promise的链接API,你可以将这些命令中的任何一个与任何Cypress命令链接起来:
cy.get([data-testid="username-input"]).type("my-username").pause();
在Playwright中进行调试
● 测试并行化
Cypress不支持本地并行运行测试。有很多库可以帮助实现并行化,但是,这并不是一件容易的事。在我以前的公司中,我们尝试并行测试以提高流水线运行时间,但结果证明付出的努力太大,以至于我们取消了它的优先级。
副作用是,开发人员很少在本地运行完整的Cypress测试套件,他们只是等待来自管道的反馈。
● 速度
Cypress在headless和headed浏览器格式下运行时,速度都很慢,这是我们希望将测试并行化的主要原因。即使在本地运行,测试也非常慢。在开发测试时,当你修改文件时,测试会自动重新加载,即使在我的MacBook Pro上也需要几秒钟。
● 测试稳定性
Cypress以不稳定而闻名,主要是在CI/CD管道上,这导致开发人员花费大量时间去追寻ghosts。我看到开发人员通常采用的笨办法是在测试中添加 cy.wait(),并在CI/CD管道中添加重试。
● 自定义命令
如果你在测试中经常使用一些常用功能,那么你很可能想为此创建一个自定义Cypress命令。遗憾的是,添加此类命令既不直观,类型也不安全(除非你做了额外的工作):
// cypress/support/commands.ts
Cypress.Commands.add('getByTestId', (selector: string, ...args: unknown[]) => {
return cy.get(`[data-testid=${selector}]`, ...args)
});
// cypress/typings/cypress.d.ts
declare namespace Cypress {
interface Chainable {
getByTestId(selector: string, ...args: unknown[]): Chainable;
}
}
// you can now use this command in your test:
it('should type username in username input', () => {
cy.getByTestId('username-input').type('my-username');
});
● 录音测试
Cypress现在提供Cypress Studio,目前这是一个实验性功能,我个人还没有尝试过。
我的Playwright使用体验
我从2023年初开始使用React,当时我的团队还没有任何UI测试。因此,我决定考虑引入Cypress。不过,我们也开始引入Web组件,并计划使用Storybook来记录和测试我们的Web组件。由于Storybook在幕后使用了Playwright,我研究了 Playwright可以提供什么。最初的诉求是:我的团队必须学习一个框架来测试Web组件和UI。
安装Playwright简直是轻而易举:不到一个小时,我就完成了所有测试。
● 编写测试所用的语法
编写Playwright测试就像编写普通的typescript代码一样简单,不需要学习特殊的API。最棒的是,你可以编写普通的async/await代码,因此你可以使用所有普通的typescript支持函数。比如:
import { test } from '@playwright/test';
test.describe('Playwright test with async/await', () => {
function sleep(milliseconds: number) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function asyncFunction(text: string) {
console.log('started asyncFunction ' + text);
await sleep(3000);
console.log('finalized asyncFunction ' + text);
}
test('should open storybook and click around', async ({ page }) => {
await page.goto('http://localhost:6006/');
await page
.getByRole('link', {
name: 'Storybook 7.1.1 is available! Your current version is: 7.0.2 Dismiss notification',
})
.click();
await page.getByRole('link', { name: 'Storybook' }).click();
await page.locator('#internal-components-overview--docs').click();
});
test('should test the async methods used in previous example', async ({
page,
}) => {
await asyncFunction('first');
await asyncFunction('second');
});
});
● 测试报告
这真是一个惊喜:我不需要做任何事情!
Playwright生成的开箱即用的报告非常棒。你还可以非常轻松地在多个浏览器上运行测试(只需更改配置,Playwright就会为你安装所需的浏览器)。
我将其配置为仅包含失败测试的跟踪信息、屏幕截图和视频;否则,就像 Cypress 一样,测试运行速度很慢。在下面的示例中,我们的第一次测试失败了。如果我们点击它,我们会得到以下信息:导致失败的异常、测试的哪些步骤失败、测试的视频以及完整的测试跟踪。
Playwright HTML 报告概述
测试失败的Playwright报告摘要
Playwright测试轨迹
● 如何运行head测试
Playwright是微软开发的,所以他们当然提供了VS Code的插件。我自己是IntelliJ的用户,但我发现当我想运行Playwright测试时,我会切换到VS Code。
与Cypress不同的是,你不必启动任何程序就能运行一个或多个测试。只需在VS Code中打开代码,然后点击通常的单元测试绿色三角形,如果过去已经运行过测试,则点击绿勾/红X。你还可以选择打开浏览器或跟踪查看器。
开发人员在这里的体验非常棒。测试打开速度快,反应灵敏。
● 如何在headless运行中访问浏览器控制台日志
与Cypress 一样,Playwright不会在Playwright运行的控制台中显示浏览器日志。为此,你可以扩展Playwright页面对象,将浏览器日志转发到控制台日志(还有其他方法,例如扩展测试对象本身)。
// utils/console.util.ts
export const configureLogForwarding = (page: Page) => {
page.on('console', (msg) => {
if (process.env.PLAYWRIGHT_LOG_TO_CONSOLE === 'true') {
switch (msg.type()) {
case 'info':
case 'log': {
// eslint-disable-next-line no-console
console.log(`Log: "${msg.text()}"`);
break;
}
case 'warning': {
// eslint-disable-next-line no-console
console.log(`Warning: "${msg.text()}"`);
break;
}
case 'assert':
case 'error': {
// eslint-disable-next-line no-console
console.log(`Error: "${msg.text()}"`);
break;
}
}
}
});
};
// tests/demo-tests.spec.ts
test.describe('Playwright test with async/await', () => {
test.beforeEach(async ({ page }) => {
configureLogForwarding(page);
hostAppNavigationPo = new HostAppNavigationPo(page);
genericAssetModalPo = new GenericAssetModalPo(page);
});
// ...
● 调试测试(Debugging Tests)
这是我最喜欢的功能之一。要调试测试,只需设置一个断点,然后右键单击绿色三角形/绿色滴点/红色x,再单击调试测试。浏览器将打开,运行将在断点处停止。从这里开始,就是开发人员习惯的正常调试会话。
在Playwright中启动调试会话
Playwright正在进行的调试会议
● 测试并行化
Playwright支持开箱即用的并行化(当然,你的被测系统当然必须能够支持并行运行的测试)。鉴于我们的CI/CD工具没有大量资源,因此速度很慢,因此我们在管道中关闭了并行测试。然而,当我编码时,我能够非常快速地在本地运行所有测试。当然,我们使用Playwright只有5个月左右,因此只有约80个测试。以下是一些非科学数据:~80个测试,在CI/CD中运行5分钟(1名工作人员),本地运行2分钟(1名工作人员),本地运行55秒(5名工作人员)。
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
/* other configuration... */
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Run tests in spec files in parallel */
fullyParallel: true,
});
● 速度
当以headless形式运行等效测试时(就像在CI/CD管道中一样),Playwright比Cypress快约1.5倍。
此外,Playwright能够在不到一秒的时间内在浏览器上启动测试(在我的MacBook Pro上)。这意味着我可以随时以任何规格运行任何测试,而无需启动Playwright服务器等。与Cypress不同的是,你必须先启动Cypress UI,然后导航到你想在该UI中运行的测试。
这为开发者带来了绝佳的体验!
● 测试稳定性
过去5个月,我们一直在编写测试,还没有遇到任何不稳定的情况,也无需在测试中使用任何等待或休眠。由于我已不在原来使用Cypress的公司工作,我很难确定将那些不稳定的测试迁移到Playwright是否会使其稳定。
● 自定义命令
Playwright提供了一种扩展基础测试的方法,这样就可以在测试过程中轻松访问自定义命令和/或页面对象。这种模式简单易用、直观且类型安全。Playwright示例:
// my-test.ts
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
export type Options = { defaultItem: string };
// Extend basic test by providing a "defaultItem" option and a "todoPage" fixture.
export const test = base.extend<Options & { todoPage: TodoPage }>({
// Define an option and provide a default value.
// We can later override it in the config.
defaultItem: ['Do stuff', { option: true }],
// Define a fixture. Note that it can use built-in fixture "page"
// and a new option "defaultItem".
todoPage: async ({ page, defaultItem }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo(defaultItem);
await use(todoPage);
await todoPage.removeAll();
},
});
// example.spec.ts
import { test } from './my-test';
test('test 1', async ({ todoPage }) => {
await todoPage.addToDo('my todo');
// ...
});
● 录制测试
Playwright提供了使用VS Code插件录制UI测试的选项。你所需要做的就是按下Record new按钮,Playwright就会打开一个浏览器。然后输入网络应用程序的URL并开始点击。Playwright录制的最大优点是,如果存在data-testid选择器(await page.getByTestId(‘dialog-title’); ),它就会选择该选择器。
Playwright 还有许多其他优点,我在本文中没有介绍,主要是因为我没有大量使用这些方面,但这里有一些示例:
● iFrames
Playwright可以与iFrames兼容,使用起来非常流畅。在我目前的工作中,我们正在使用iFrames,但在我之前的工作中(当时我在Cypress工作),我们没有使用 iFrames。不过,据我所知,你需要安装Cypress插件才能测试iFrames。
● Web-app服务器
Cypress和Playwright都要求你的应用程序在运行时才能进行测试。对Cypress的一个普遍要求是,该框架能够在运行测试前启动被测系统。我个人在本地和在CI/CD管道中运行测试时,应用程序始终处于运行状态。不过,Playwright也听到了开发人员的愿望:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Run your local dev server before starting the tests.
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});
● Forbid only关键字
一个常见的用例是,当开发人员想要运行单个测试时,他们将其标记为only. 不幸的是,他们经常忘记在提交之前删除它。Playwright附带了一个配置来检测这一点,而使用Cypress时我们必须添加eslint规则。
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
});
// example.spec.ts
test.describe('Playwright test with async/await', () => {
test.only('should test the async methods used in previous example', async ({
page,
}) => {
await asyncFunction1();
await asyncFunction2();
});
// more tests...
● 多页面
Cypress在其自己的Web应用程序内运行Web应用程序,这会限制Cypress一次只能在一页上运行测试。另一方面,Playwright可在每个浏览器中打开多个BrowserContext和/或多个页面,所有这些页面都有不同的网站/域等,并可在所有这些页面上运行测试。
● 多语言支持
在Cypress中,您可以选择使用JavaScript或TypeScript编写测试。然而,在Playwright中,编写测试的语言选项更加多样化。你可以选择使用JavaScript、TypeScript、Java、Python或 .NET编写测试,从而为开发人员提供了更大的灵活性,让他们可以使用自己喜欢的编程语言来实现测试自动化。
结论
多年来,我一直是Cypress的粉丝,也是UI测试的坚定拥护者,现在要开始使用一个我不确定是否会像Cypress一样优秀的新框架,这并不是一个容易的决定。尽管如此,我还是惊喜地发现Playwright不仅达到了Cypress的预期,而且在我能想到的所有方面都超过了Cypress。
最后,Cypress和Playwright的开发者社区都相当庞大。尽管Cypress仍然更受欢迎,但随着越来越多的开发者发现Playwright的开发者体验是如此令人惊叹,Playwright社区也在快速发展。
截至2023年7月29日的对比数据
建议任何已经在使用Cypress或正在考虑进入UI测试领域的人,将Playwright作为一个值得考虑的选择。
最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!