cypress Introduce
支持e2e测试和component测试。
Cypress跨浏览器测试
cypress支持多种浏览器,
Chrome-family browsers
(including Electron and Chromium-based Microsoft Edge),
WebKit
(Safari’s browser engine), and Firefox. 在本地或者CI执行测试的时候,除了electron外,其他类型的浏览器都需要提前安装,cypress不提供虚拟环境。支持测试分组,也支持只运行一部分的测试,同时还可以并行在不同的浏览器执行测试。
-
Chrome 80 and above.
-
Edge 80 and above.
-
Firefox 86 and above.
目前12的版本的cypress对于Safari这种webkit的浏览器还是实验阶段,需要打开experimentalWebKitSupport在配置文件中。安装
npm install --save-dev playwright-webkit
,在webkit中还不支持origin方法;cy.intercept()的forceNetworkError选项不可用;cy.type()设置type = number的时候没有四舍五入到指定的长度。
由于cypress会自动寻找本地machine中的浏览器加载到browser list中,但是如果想要只使用某一种浏览器内核进行测试,可以在配置文件中设置setupNode Event,filter目标浏览器。
如果filter的结果是空的,也就是没有目标浏览器被找到,那会执行默认寻找到的浏览器。
const { defineConfig } = require('cypress') module.exports = defineConfig({ // setupNodeEvents can be defined in either // the e2e or component configuration e2e: { setupNodeEvents(on, config) { // inside config.browsers array each object has information like // { // name: 'chrome', // channel: 'canary', // family: 'chromium', // displayName: 'Canary', // version: '80.0.3966.0', // path: // '/Applications/Canary.app/Contents/MacOS/Canary', // majorVersion: 80 // } return { browsers: config.browsers.filter( (b) => b.family === 'chromium' && b.name !== 'electron' ), } }, }, })
设置浏览器的方式:
-
default configuration
-
System
environment variables
cypress启动浏览器和直接开本地的浏览器不同,它是一个全新的环境,可以在启动之前对浏览器进行设置,设置可以写在configuration文件中。cypress启动的浏览器不包括third part extension,如果要使用的话需要在cypress启动浏览器后重新在该浏览器中安装,这样后面的测试将可以使用。
cypress启动的浏览器可以自动的disable 一些barriers:
-
ignore 证书错误
-
允许关闭的pop-ups
-
禁用 save password功能
-
禁用自动密码填充
-
禁用询问设置为主浏览器
-
禁用设备发现通知
-
禁用语言翻译
-
禁用恢复会话
-
禁用后台网络流量
-
禁用背景和后台渲染
-
禁用使用mic和camera的权限提示
-
禁用自动播放的手势要求
Cypress 跨域
通常情况下,cypress只允许同一个测试在同一个域名下,除非使用了origin才能使用不同的域名。当然如果是出现了第三方的iframe或者component也需要使用window.postMessage直接通讯,如果不行的话需要关闭web security,
chromeWebSecurity: false,
experimentalModifyObstructiveThirdPartyCode: true,
在Google和salesforce相关的页面使用origin得时候需要在配置中添加
module.exports = defineConfig({ e2e: { experimentalSkipDomainInjection: [ '*.salesforce.com', '*.force.com', '*.google.com', ], }, })
experimentalSkipDomainInjection这个设置是为了替换document.domain功能。
superdomain
what is a superdomain
相同的superdomain跟相同的origin概念比较相似,也就是两个url在协议、端口、host都相同。再用一个superdomain中数据共享,cypress使用document.domain的注入实现这个功能,所以在同一个测试中可以visit同一个域中不同的url。在V12版本之前都没有提供在同一个用例中访问跨域url的能力。现在有了origin方法。可以重新定义domain,完成在同一个测试中访问不同domain的url。
how to achieve communication in the same superdomain
同一个superdomain下,cypress会使用documen.domain注入的方式识别。但是这种方式可能会导致一些问题,所以在V12.0版本的cypress使用了experimentalSkipDomainInjection代替以解决一些issue。
当使用origin跨域后,新的域名下要重新传递token或者cookie。
Cypress debugger
cy.visit('/my/page/path') cy.get('[data-testid="selector-in-question"]').then(($selectedElement) => { // Debugger is hit after the cy.visit // and cy.get commands have completed debugger }) })
Debugger 必须要在异步执行完操作后才会生效。直接添加debugger是不生效的。
直接使用debug打印相关的信息在console中
cy.visit('/my/page/path') cy.get('[data-testid="selector-in-question"]').debug()
check an error steps
-
error name (cypress error 或 assertionError)
-
Error message
-
code frame file
-
code frame
-
view stack trace
-
print to console button
Environment variables
-
可以在config文件中设定baseURL,这样当cypress调用request和visit的时候都会自动加上baseurl的值,不需要单独指定。
const { defineConfig } = require('cypress') module.exports = defineConfig({ projectId: '128076ed-9868-4e98-9cef-98dd8b705d75', env: { login_url: '/login', products_url: '/products', }, })
-
Create a cypress.env.json
在文件中定义的环境变量会覆盖配置文件中的环境变量。专门用来存环境变量,在cypress构建过程中就已经自动生成了。
-
可以在测试套件中或者单一的测试里面使用env添加环境变量。
// change environment variable for single suite of tests describe( 'test against Spanish content', { env: { language: 'es', }, }, () => { it('displays Spanish', () => { cy.visit(`https://docs.cypress.io/${Cypress.env('language')}/`) cy.contains('¿Por qué Cypress?') }) } )
Module API
可以通过node js 运行cypress,从而使得cypress作为一个node module被分离出来。这样对于想要在测试执行后看到测试结果很有用,这样使用
Cypress Module API recipe
后就能:
-
测试失败后,发送带有screenshot的错误通知
-
重新运行单个的spec 文件
-
启动其他构建或脚本
// e2e-run-tests.js const cypress = require('cypress') cypress.run({ reporter: 'junit', browser: 'chrome', config: { baseUrl: 'http://localhost:8080', video: true, }, env: { login_url: '/login', products_url: '/products', }, })
Network Request
cypress有能力可以选择stub response或者让request 进入服务器处理。一般对于request的场景有:断言request body\status code\ url\hearders,模拟request body\status code\hearders,延迟答复和等待答复。
serve response:
使用serve的响应进行测试的优点是:测试更贴近实际环境,能够提高测试的信心,但是响应速度比较慢,产生实际的数据,需要清理。
stub response:
使用cy.intercept()可以模拟请求,使用stub优点是可以自己控制request的响应,对于服务端和客户端的代码没有影响,速度快。
// stub out the response without interacting with a real back-end cy.intercept('POST', '/users', (req) => { req.reply({ headers: { Set-Cookie: 'newUserName=Peter Pan;' }, statusCode: 201, body: { name: 'Peter Pan' }, delay: 10, // milliseconds throttleKbps: 1000, // to simulate a 3G connection forceNetworkError: false // default }) }) // stub out a response body using a fixture cy.intercept('GET', '/users', (req) => { req.reply({ statusCode: 200, // default fixture: 'users.json' }) })
Cy.wait()
cy.intercept({ method: 'POST', url: '/myApi', }).as('apiCheck') cy.visit('/') cy.wait('@apiCheck').then((interception) => { assert.isNotNull(interception.response.body, '1st API call has data') }) cy.wait('@apiCheck').then((interception) => { assert.isNotNull(interception.response.body, '2nd API call has data') }) cy.wait('@apiCheck').then((interception) => { assert.isNotNull(interception.response.body, '3rd API call has data') })
.as(‘apiCheck’)可以对stub的接口进行别名的修饰,wait可以使用别名等待接口调用完毕后进行一些相应的测试。当wait没有收到实际的结果返回的时候,会有更清晰的报错信息。使用wait可以对实际的object进行断言。
// spy on POST requests to /users endpoint cy.intercept('POST', '/users').as('new-user') // trigger network calls by manipulating web app's // user interface, then cy.wait('@new-user').should('have.property', 'response.statusCode', 201) // we can grab the completed interception object // again to run more assertions using cy.get(<alias>) cy.get('@new-user') // yields the same interception object .its('request.body') .should( 'deep.equal', JSON.stringify({ id: '101', firstName: 'Joe', lastName: 'Black', }) ) // and we can place multiple assertions in a // single "should" callback cy.get('@new-user').should(({ request, response }) => { expect(request.url).to.match(/\/users$/) expect(request.method).to.equal('POST') // it is a good practice to add assertion messages // as the 2nd argument to expect() expect(response.headers, 'response headers').to.include({ 'cache-control': 'no-cache', expires: '-1', 'content-type': 'application/json; charset=utf-8', location: '<domain>/users/101', }) }) cy.wait('@new-user').then(console.log) 可以把log输出到控制台观察
Parallelization 并行测试
当项目上的测试用例比较多的时候,为了减少运行时间,可以采用在多台机器并行测试的方式。当然也可以让并行的测试在同一台机器上,起多个浏览器,但是这样会消耗这台机器很多资源。
想要执行并行测试,首先要在CI上配置多台可用的虚拟机,cypress会把所有收集到的spec测试文件打包为一个spec list给到cypress cloud,cypress cloud有个balance strategy,可以收集分析执行的数据,这个数据通过运行时候的– record命令得到,分析后会将合适的spec文件分给不同的机器运行。可以按照label名称分组,cypress run –record –group 2x-chrome –browser chrome –parallel 指定不同的数量进行并行测试。
除此之外,不使用cypress cloud的话,还可以使用group分组,分组方式可以按照浏览器分组,不同的测试运行在不同的浏览器中;可以使用Grouping by spec context,cypress run –record –group package/admin –spec ‘cypress/e2e/packages/admin/
*/
‘通过spec名称进行分组。
Screenshots and Videos
使用cypress run 命令运行的时候,会自动抓取错误用例的截图和进行录屏,但是使用cypress open不会有这样的效果。
可以在configuration文件中设置screenshotOnRunFailure 选项,true或者false。默认截图文件都会被存储在cypress/shot screen文件下,在运行新的测试之前,cypress run会清楚已有的截图文件,不想清除则可以在配置文件中将 trashAssetsBeforeRuns的值设置为false。
在cypress run结束后会自动进行视频的压缩,默认大小是32crf,也可以使用vedio Compression在设置文件中自定义。为了更精细化的控制视频选项,可以使用after:spec设置:
const { defineConfig } = require('cypress') const fs = require('fs') module.exports = defineConfig({ // setupNodeEvents can be defined in either // the e2e or component configuration e2e: { setupNodeEvents(on, config) { on('after:spec', (spec, results) => { if (results && results.video) { // Do we have failures for any retry attempts? const failures = results.tests.some((test) => test.attempts.some((attempt) => attempt.state === 'failed') ) if (!failures) { // delete the video if the spec passed and no tests retried fs.unlinkSync(results.video) } } }) }, }, })
CI中为了避免大量的视频文件,我们使用配置禁止上传CI同时只有在用例失败的时候录制视频:
"video": { "videoUploadOnPasses": false, "keepVideoForFailedTests": true, }
Test Retries
在测试一些比较复杂的系统或者复杂的业务逻辑的时候,总会出现一些test flaky,不稳定的测试会导致整个测试看上去都不健康,所以重试机制是必要的。默认情况下是不会进行重试的,想要触发重试机制需要在配置文件中进行设置:
{ "retries": { // Configure retry attempts for `cypress run` // Default is 0 "runMode": 2, // Configure retry attempts for `cypress open` // Default is 0 "openMode": 0 } }
run mode 和 open mode分别对应cypress run 和 cypress open两种命令。如果在配置文件中配置就会是全局的重试,所有的测试用例都会进行重试,当然也可以针对单独的测试或者单独的test suite,在run 模式下重试的用例中,也会进截图,截图信息会带有attempt的次数。
IDE extension
Plugins
cypress的本质是在浏览器外部起一个node server,直接访问浏览器dom、网络请求和本地存储。plugin可以帮助我们在访问外部的node serve,可以在cypress的各个生命周期执行不同行为的code。
configuration
run lifecycle
可以定义启动测试前和测试结束后的事件
spec lifecycle
可以定义spec文件执行前和执行后的事件,也可以指定spec文件执行完成后删除vedio等操作
browser launching
可以指定在浏览器启动前加载一些extension,或者指定使用什么浏览器内核、浏览器类型
screenshot handing
截图后可以设定一些事件,使用after:screenshot,可以修改截图名称,或者展示图片更详细的信息以及在图上截图。
Cy.task()
它允许在node中编写任何代码来完成浏览器完成不了的功能。比如操作数据库,持久化会话数据(在cypress中会话数据会被刷新)或者启动另外的node serve(webdriver实例或其他浏览器)
const { defineConfig } = require('cypress') module.exports = defineConfig({ // setupNodeEvents can be defined in either // the e2e or component configuration e2e: { setupNodeEvents(on, config) { on('task', { async 'db:seed'() { // seed database with test data const { data } = await axios.post(`${testDataApiEndpoint}/seed`) return data }, // fetch test data from a database (MySQL, PostgreSQL, etc...) 'filter:database'(queryPayload) { return queryDatabase(queryPayload, (data, attrs) => _.filter(data.results, attrs) ) }, 'find:database'(queryPayload) { return queryDatabase(queryPayload, (data, attrs) => _.find(data.results, attrs) ) }, }) }, }, })
Reporter
由于cypress是基于mocha开发的,所以默认的reporter也支持mocha的reporter,基于spec的,展示在命令行中的,同时cypress团队也引入了默认的junit和teamcity的报告。最后也支持其他任何的第三方报告。
每一个spec执行完后,都会生成reporter报告。这样会不断的替换掉原有的报告,所以需要使用[hash]将每个spec的报告区分开。配置如下:
const { defineConfig } = require('cypress') module.exports = defineConfig({ reporter: 'junit', reporterOptions: { mochaFile: 'results/my-test-output-[hash].xml', }, })
我们在本地想要看到的reporter和在CI上想要看到的reporter可能不一样,cypress还支持我们使用多种reporter的配置。
需要额外安装两个依赖:
-
cypress-multi-reporters
: enables multiple reporters -
mocha-junit-reporter
the actual junit reporter, as we cannot use the
junit
reporter that comes with Cypress
const { defineConfig } = require('cypress') module.exports = defineConfig({ reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { mochaFile: 'cypress/results/results-[hash].xml', }, })
使用npm命令完成多个reporter的生成,添加下面的命令在package.json中:
{ "scripts": { "delete:reports": "rm cypress/results/* || true", "combine:reports": "jrm cypress/results/combined-report.xml \"cypress/results/*.xml\"", "prereport": "npm run delete:reports", "report": "cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json", "postreport": "npm run combine:reports" } }
🌰:使用mocha generate生成报告的解决方案
配置文件configuration.json
const { defineConfig } = require('cypress') module.exports = defineConfig({ reporter: 'mochawesome', reporterOptions: { reportDir: 'cypress/results', overwrite: false, html: false, json: true, }, })
Command line 实现:
cypress run --reporter mochawesome \ --reporter-options reportDir="cypress/results",overwrite=false,html=false,json=true
运行后每个spec都会生成一个json文件,我们需要使用
npx mochawesome-merge "cypress/results/*.json" > mochawesome.json
进行合并。原理是使用了mocha generation reporter:
GitHub – adamgruber/mochawesome-report-generator: Standalone mochawesome report generator. Just add test data.
最后使用
npx marge mochawesome.json
我们就能生成精美的html报告了。
Cypress API
Queries
.as()
可以起一个aliasname,不能直接跟cy使用,一般的使用场景有:
-
dom element:cy.get(‘id-number’).as (‘username’), cy.get(‘@username’).type(‘abc123’)
-
Intercept stub response: cy.intercept(“PUT”, “/users”, {fixture: ‘user’}).as(‘editUser’), cy.get(‘form’).submit(), cy.wait(‘@editUser’).its(‘url’).should(‘contain’, ‘users’)
-
beforeEach(() => { cy.fixture(‘users-admins.json’).as(‘admins’) })
it(‘the users fixture is bound to this.admins’, function () { cy.log(
There are ${this.admins.length} administrators.
) })
describe('A fixture', () => { describe('alias can be accessed', () => { it('via get().', () => { cy.fixture('admin-users.json').as('admins') cy.get('@admins').then((users) => { cy.log(`There are ${users.length} admins.`) }) }) it('via then().', function () { cy.fixture('admin-users.json').as('admins') cy.visit('/').then(() => { cy.log(`There are ${this.admins.length} admins.`) }) }) }) describe('aliased in beforeEach()', () => { beforeEach(() => { cy.fixture('admin-users.json').as('admins') }) it('is bound to this.', function () { cy.log(`There are ${this.admins.length} admins.`) }) }) })
children()
需要从一个dom元素上使用,不能直接cy.children()
cy.get('.left-nav>.nav').children().should('have.length', 8)
Closet ()
选取最近的一个合适的元素, 不能直接使用,需要在一个dom元素后使用
cy.get('p.error').closest('.banner')
contains()
可以查找一个目标dom元素文本多于查找文本的情况。需要接在一个dom元素上使用。contains的参数可以是文本、数字、选择器,可以使用case Match = false屏蔽掉case sensitivity
cy.get('div').contains('capital sentence', { matchCase: false })
cy.get('form').contains('submit the form!').click()
Document()
获取当前页面的window.document(),
cy.document().its('contentType').should('eq', 'text/html')
Eq ()
可以获得一个dom元素下特定索引的元素
cy.get('li').eq(1).should('contain', 'siamese') // true
filter()
获得一个dom元素下特定选择器的元素
cy.get('td').filter('.users') // Yield all el's with class '.users'
Find()
获得特定dom元素的特定后裔元素,find要找的元素要跟dom节点的元素存在parent-son related
first()
获取符合条件的第一个dom元素
cy.get('selector').first()
Focused()
获取当前被关注的元素
get()
获取一个或者多个符合选择器或者别名的元素,有个选项是includeShadowDom,是否深入shadow DOM 中查找元素,默认是false。ID选择的时候加#,class选择的时候加
.
hash()
获取当前活动页面url的hash值。
invoke()
可以在已有的对象上启用一个新的方法,同时invoke只能被call一次,如果invoke后面有其他的操作,异步的特性就会导致它被call多次。invoke可以包含函数,带参数的函数,数组或者第三方组件功能。
its()
获取在已经获得的元素的相关信息
Last()
获取一组元素中的最后一个元素
Cy.location()
获取当前活跃页面的window.location信息,直接使用cy chain就可以。
cy.location().should((loc) => { expect(loc.href).to.include('commands/querying') })
Cy.next()
获取一组dom元素后面最近的sibling element,需要跟着一个dom元素使用,不能直接和cy建立连接
Cy.nextAll()
获取一组dom元素所有相邻的sibling element,需要跟着一个dom元素使用,不能直接和cy建立连接
Cy.not()
对于一组已经获得dom元素过滤,去掉not筛选的元素。
Cy.parent()
获取获取元素的父级元素,需要跟着一个dom元素使用,不能直接和cy建立连接
cy.parents()
Cy.parentUntil()
cy.root()
获取一组元素中的根元素
cy.shadow()
适用于某个元素包含在另一个元素内部,需要使用该元素的时候,可以cy.get(外部元素).shadow().find(内部元素)/contains().click(),shadow可以打开rootshadow开关。或者使用cy.get(内部元素,{includeShadowDom: true})
有时候在Chrome中可能发生点击某个元素错位置的情况,这是由于协议不同导致的,需要使用position:top解决:
cy.get('#element') .shadow() .find('[data-test-id="my-button"]') .click({ position: 'top' })
cy.siblings()
获取元素的同胞元素,需要跟着一个dom元素使用,不能直接和cy建立连接
cy.title()
获取当前活跃页面的document.title()
cy.url()
获取当前活跃页面的url
Assertions
cypress 绑定了chai断言库,同时还增加了jquery-chai,chai-sinon扩展。
And 的语法
.and(chainers) .and(chainers, value) .and(chainers, method, value) .and(callbackFn)
Should 的语法
.should(chainers) .should(chainers, value) .should(chainers, method, value) .should(callbackFn)
Chainer syntax:
Actionability
-
.click()
-
.dbclick()
-
.rightclick()
-
.check() 勾选checkbox或者radio
-
.clear()
-
.scrollIntoView()
-
.scrollTo()
-
.select()
-
.selectFile()
-
.trigger()
-
.type()
-
.uncheck()
Plugins