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。

cypress普及吗 cypress intercept_firefox

 

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 testsdescribe(
  '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.jsconst 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-endcy.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 endpointcy.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

cypress普及吗 cypress intercept_cypress普及吗_02

 

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