Skip to main content
Cypress应用

从 Protractor 迁移到 Cypress

info
您将学习到
  • 如何从 Protractor 迁移到 Cypress
  • 使用 Cypress 进行端到端测试的优势
  • 如何在 Cypress 中操作 DOM 和编写断言
  • 使用 Angular schematic 配置 Cypress

Protractor 曾是 Angular 和 AngularJS 应用中流行的端到端测试工具,但自 Angular 12 起,新 Angular 项目已不再包含它。我们为您准备了这份迁移指南,帮助您和团队从 Protractor 过渡到 Cypress。

首先,通过一个快速代码示例看看 Cypress 对 Protractor 用户有多友好。以下场景测试用户能否注册新账户。

tip

要查看您自己的测试代码如何转换,可将其粘贴到交互式 Cypress Migrator 工具,该工具会生成等效的 Cypress 代码。

迁移前:Protractor
describe('Authorization tests', () => {
it('allows the user to signup for a new account', () => {
browser.get('/signup')
element(by.css('#email-field')).sendKeys('user@email.com')
element(by.css('#confirm-email-field')).sendKeys('user@email.com')
element(by.css('#password-field')).sendKeys('testPassword1234')
element(by.cssContainingText('button', 'Create new account')).click()

expect(browser.getCurrentUrl()).toEqual('/signup/success')
})
})
迁移后:Cypress
describe('Authorization Tests', () => {
it('allows the user to signup for a new account', () => {
cy.visit('/signup')
cy.get('#email-field').type('user@email.com')
cy.get('#confirm-email-field').type('user@email.com')
cy.get('#password-field').type('testPassword1234')
cy.get('button').contains('Create new account').click()

cy.url().should('include', '/signup/success')
})
})

使用 Cypress 的优势

许多开发者深知端到端测试的必要性,但常因测试脆弱或耗时过长而放弃。Cypress 不仅确保测试可靠,还提供工具使测试成为开发助力而非阻碍。

在浏览器中交互式测试

Protractor 测试时浏览器自动化速度过快,难以观察。Cypress 的命令日志实时显示测试过程,点击命令可查看 DOM 快照,观察应用在测试中的真实渲染 UI用户交互行为。此外,选择器 playgroundCypress Studio 帮助快速定位 CSS 选择器。

更快的反馈循环

Cypress 在文件保存时自动重新运行测试,结合编辑器与浏览器并排工作,实现高效迭代。

测试时间回溯

通过 DOM 快照,可回溯测试执行中任意时间点的应用状态,模拟真实用户行为。

无头模式下的可视化

Cypress 在无头模式下自动截取失败截图、录制测试视频,并通过 Test Replay 在 Cypress Cloud 中重现测试过程。

测试重试

复杂应用中可能出现“脆弱测试”。Cypress 支持测试重试,Cypress Cloud 还能检测 CI/CD 中的脆弱测试

开始使用

推荐安装方式

使用官方 Cypress Angular schematic

ng add @cypress/schematic

这会安装 Cypress、添加运行脚本、生成基础文件,并可选移除 Protractor 配置。安装后,通过以下命令启动 Cypress:

ng e2e

或直接运行:

ng run {your-project-name}:cypress-open

无头模式运行:

ng run {your-project-name}:cypress-run
tip

此 schematic 基于 Briebug 团队的 @briebug/cypress-schematic 开发。

手动安装

npm install cypress --save-dev

安装 concurrently 并行启动应用和 Cypress:

npm install concurrently --save-dev

更新 package.json 脚本:

package.json
{
"scripts": {
"cy:open": "concurrently \"ng serve\" \"cypress open\"",
"cy:run": "concurrently \"ng serve\" \"cypress run\""
},
"dependencies": { ... },
"devDependencies": { ... }
}

运行:

npm run cy:open

DOM 操作

获取 DOM 元素

获取单个元素

迁移前:Protractor
element(by.css('.my-class'))
element(by.id('my-id'))
element(by.name('field-name'))
element(by.cssContainingText('.my-class', 'text'))
element(by.linkText('text'))
迁移后:Cypress
cy.get('.my-class')
cy.get('#my-id')
cy.get('input[name="field-name"]')
cy.get('.my-class').contains('text')
cy.contains('text')

获取多个元素

迁移前:Protractor
element.all(by.tagName('li'))
element.all(by.css('.list-item'))
element.all(by.name('field-name'))
迁移后:Cypress
cy.get('li')
cy.get('.list-item')
cy.get('input[name="field-name"]')
info

推荐使用 Cypress Testing Library 扩展 findByfindAllBy 命令。

选择器 Playground

类似 Protractor 的 Element Explorer,Cypress 提供选择器 Playground 帮助定位唯一选择器。

与 DOM 元素交互

迁移前:Protractor
element(by.css('button')).click()
element(by.css('input')).sendKeys('my text')
element(by.css('input')).clear()
element.all(by.css('[type="checkbox"]')).first().click()
element(by.css('[type="radio"][value="radio1"]')).click()
element.all(by.css('[type="checkbox"][checked="true"]')).first().click()
element(by.cssContainingText('option', 'my value')).click()
browser.actions().mouseMove(element(by.id('my-id'))).perform()
迁移后:Cypress
cy.get('button').click()
cy.get('input').type('my text')
cy.get('input').clear()
cy.get('[type="checkbox"]').first().check()
cy.get('[type="radio"]').check('radio1')
cy.get('[type="checkbox"]').not('[disabled]').first().uncheck()
cy.get('select[name="optionsList"]').select('my value')
cy.get('#my-id').scrollIntoView()
info

更多交互方法见官方文档

断言

长度断言

迁移前:Protractor
expect(list.count()).toBe(3)
迁移后:Cypress
cy.get('li.selected').should('have.length', 3)

类断言

迁移前:Protractor
expect(input.getAttribute('class')).not.toContain('disabled')
迁移后:Cypress
cy.get('form').find('input').should('not.have.class', 'disabled')

值断言

迁移前:Protractor
expect(textarea.getAttribute('value')).toBe('foo bar baz')
迁移后:Cypress
cy.get('textarea').should('have.value', 'foo bar baz')

文本内容断言

迁移前:Protractor
expect(element(by.id('user-name')).getText()).toBe('Joe Smith')
expect(element(by.id('address')).getText()).toContain('Atlanta')
expect(parent.getText()).not.toContain('click me')
element(by.id('greeting').getText()).toMatch(/^Hello/)
迁移后:Cypress
cy.get('#user-name').should('have.text', 'Joe Smith')
cy.get('#address').should('include.text', 'Atlanta')
cy.get('a').parent('span.help').should('not.contain', 'click me')
cy.get('#greeting').invoke('text').should('match', /^Hello/)
cy.contains('#a-greeting', /^Hello/)

可见性断言

迁移前:Protractor
expect(button.isDisplayed()).toBe(true)
迁移后:Cypress
cy.get('button').should('be.visible')

存在性断言

迁移前:Protractor
expect(element(by.id('loading')).isPresent()).toBe(false)
迁移后:Cypress
cy.get('#loading').should('not.exist')

状态断言

迁移前:Protractor
expect(radio.isSelected()).toBeTruthy()
迁移后:Cypress
cy.get(':radio').should('be.checked')

CSS 断言

迁移前:Protractor
expect(element(by.css('.completed')).getCssValue('text-decoration')).toBe('line-through')
expect(element(by.id('accordion')).getCssValue('display')).not.toBe('none')
迁移后:Cypress
cy.get('.completed').should('have.css', 'text-decoration', 'line-through')
cy.get('#accordion').should('not.have.css', 'display', 'none')

禁用属性断言

迁移前:Protractor
expect(input.isEnabled()).toBe(false)
迁移后:Cypress
cy.get('#example-input')
.should('be.disabled')
.invoke('prop', 'disabled', false)
.should('be.enabled')
.and('not.be.disabled')

Cypress 的重试机制能减少误报。

网络处理

网络监听

Cypress 的 intercept API 可监听和管理网络请求:

cy.intercept('/users/**')
cy.get('button').contains('Load More')

网络桩

模拟网络错误或自定义响应:

cy.intercept('GET', 'https://api.openweathermap.org/data/2.5/weather?q=Atlanta', { statusCode: 500 })
cy.get('.weather-forecast').contains('Weather Forecast Unavailable')

导航

迁移前:Protractor
browser.get('/about')
browser.navigate().forward()
browser.navigate().back()
迁移后:Cypress
cy.visit('/about')
cy.go('forward')
cy.go('back')

自动重试与等待

Cypress 自动重试 DOM 查询命令,无需手动等待:

迁移前:Protractor
element(by.css('button')).click()
browser.waitForAngular()
expect(by.css('.list-item').getText()).toEqual('my text')
迁移后:Cypress
cy.get('button').click()
cy.get('.list-item').contains('my text')

Cypress 与 WebDriver 控制流

Cypress 命令异步执行但串行排队,类似 Protractor 的控制流但更直观:

cy.get('button').click()
cy.get('input').type('my text')

使用页面对象

Protractor 页面对象

const page = {
login: () => {
element(by.css('.username')).sendKeys('my username')
element(by.css('.password')).sendKeys('my password')
element(by.css('button')).click()
},
}

Cypress 页面对象

const page = {
login: () => {
cy.get('.username').type('my username')
cy.get('.password').type('my password')
cy.get('button').click()
},
}

或使用自定义命令:

Cypress.Commands.add('login', (username, password) => {
cy.get('.username').type(username)
cy.get('.password').type(password)
})

持续集成

Cypress 支持所有主流 CI 环境,详见:

并行化

Cypress Cloud 支持跨多台 CI 机器并行运行测试文件

cypress run --record --parallel
tip
调试Cypress Cloud测试运行?

不要依赖本地复现失败条件或人工解析测试产物。使用 测试回放功能,完整重现录制运行期间的测试执行过程,获得全面的调试能力。

Angular Schematic 配置

指定浏览器运行

"cypress-open": {
"builder": "@cypress/schematic:cypress",
"options": {
"watch": true,
"headless": false,
"browser": "chrome"
}
}

记录测试到 Cypress Cloud

"cypress-run": {
"builder": "@cypress/schematic:cypress",
"options": {
"record": true,
"key": "your-cypress-dashboard-recording-key"
}
}

自定义配置文件

"options": {
"configFile": "cypress.production.config.js"
}

CI 并行模式

"options": {
"parallel": true,
"record": true,
"key": "your-cypress-dashboard-recording-key"
}

代码覆盖率

参考代码覆盖率指南评估测试覆盖范围。

下一步

更多端到端测试信息,请查阅官方文档

caution

发现指南有误?请发起讨论

常见问题

必须立即替换所有 Protractor 测试吗?

不必。可逐步迁移 Protractor 测试到 Cypress。

Protractor 和 Cypress 能否共存?

可以。Protractor 测试保留在 e2e 目录,Cypress 测试放在同级 cypress 文件夹。

.
├── cypress
├── e2e
├── src
└── ...