Cypress 基础知识学习

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'
        ),
      }
    },
  },
})

设置浏览器的方式:

  1. default configuration

  2. The Cypress environment file

  3. System environment variables

  4. Command Line arguments

  5. setupNodeEvents

cypress启动浏览器和直接开本地的浏览器不同,它是一个全新的环境,可以在启动之前对浏览器进行设置,设置可以写在configuration文件中。cypress启动的浏览器不包括third part extension,如果要使用的话需要在cypress启动浏览器后重新在该浏览器中安装,这样后面的测试将可以使用。

cypress启动的浏览器可以自动的disable 一些barriers:

  1. ignore 证书错误

  2. 允许关闭的pop-ups

  3. 禁用 save password功能

  4. 禁用自动密码填充

  5. 禁用询问设置为主浏览器

  6. 禁用设备发现通知

  7. 禁用语言翻译

  8. 禁用恢复会话

  9. 禁用后台网络流量

  10. 禁用背景和后台渲染

  11. 禁用使用mic和camera的权限提示

  12. 禁用自动播放的手势要求

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

  1. error name (cypress error 或 assertionError)

  2. Error message

  3. code frame file

  4. code frame

  5. view stack trace

  6. print to console button

Environment variables

  1. 可以在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',
  },
})
  1. Create a cypress.env.json

在文件中定义的环境变量会覆盖配置文件中的环境变量。专门用来存环境变量,在cypress构建过程中就已经自动生成了。

  1. 可以在测试套件中或者单一的测试里面使用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后就能:

  1. 测试失败后,发送带有screenshot的错误通知

  2. 重新运行单个的spec 文件

  3. 启动其他构建或脚本

// 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的配置。

需要额外安装两个依赖:

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使用,一般的使用场景有:

  1. dom element:cy.get('id-number').as ('username'), cy.get('@username').type('abc123')

  2. Intercept stub response: cy.intercept("PUT", "/users", {fixture: 'user'}).as('editUser'), cy.get('form').submit(), cy.wait('@editUser').its('url').should('contain', 'users')

  3. 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

  1. .click()

  2. .dbclick()

  3. .rightclick()

  4. .check() 勾选checkbox或者radio

  5. .clear()

  6. .scrollIntoView()

  7. .scrollTo()

  8. .select()

  9. .selectFile()

  10. .trigger()

  11. .type()

  12. .uncheck()

Plugins

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值