Cypress 的学习笔记

docs.cypress.io/guides/core-concepts/introduction-to-cypress#Subject-Management

describe('Post Resource', () => {
  it('Creating a New Post', () => {
    cy.visit('/posts/new') // 1.

    cy.get('input.post-title') // 2.
      .type('My First Post') // 3.

    cy.get('input.post-body') // 4.
      .type('Hello, world!') // 5.

    cy.contains('Submit') // 6.
      .click() // 7.

    cy.url() // 8.
      .should('include', '/posts/my-first-post')

    cy.get('h1') // 9.
      .should('contain', 'My First Post')
  })
})

上述 cypress 代码,很像自然语言。

cypress 的 语法,cy.get('.my-selector'),很像jQuery: cy.get('.my-selector')

事实上,cypress 本身就 bundle 了 jQuery:

支持类似 jQuery 的链式调用:

cy.get('#main-content').find('.article').children('img[src^="/static"]').first()

只是有一点需要特别注意:

ct.get 并不会像 jQuery 那样,采用同步的方式返回待读取的元素。Cypress 的元素访问,采取异步方式完成。

因为 jQuery 的同步访问机制,我们在调用元素查询 API 之后,需要手动查询其结果是否为空:

// $() returns immediately with an empty collection.const $myElement = $('.element').first()// Leads to ugly conditional checks// and worse - flaky tests!if ($myElement.length) {
  doSomething($myElement)}

而 Cypress 的异步操作,导致待读取的元素真正可用时,其结果才会被作为参数,传入回调函数:

cy
  // cy.get() looks for '#element', repeating the query until...  .get('#element')

  // ...it finds the element!  // You can now work with it by using .then  .then(($myElement) => {
    doSomething($myElement)
  })

In Cypress, when you want to interact with a DOM element directly, call .then() with a callback function that receives the element as its first argument.

也就是说,Cypress 内部帮我们封装了 retry 和 timeout 重试机制。

When you want to skip the retry-and-timeout functionality entirely and perform traditional synchronous work, use Cypress.$.

如果想回归到 jQuery 那种同步读取元素的风格,使用 Cypress.$ 即可。

// Find an element in the document containing the text 'New Post'cy.contains('New Post')// Find an element within '.main' containing the text 'New Post'cy.get('.main').contains('New Post')

Cypress commands do not return their subjects, they yield them. Remember: Cypress commands are asynchronous and get queued for execution at a later time. During execution, subjects are yielded from one command to the next, and a lot of helpful Cypress code runs between each command to ensure everything is in order.

Cypress 命令并不会直接返回其工作的目标,而是 yield 这些目标。Cypress 命令以异步的方式执行,命令被插入到队列里,并不会立即执行,而是等待调度。当命令真正执行时,目标对象经由前一个命令生成,然后传入下一个命令里。命令与命令之间,执行了很多有用的 Cypress 代码,以确保命令执行顺序和其在 Cypress 测试代码里调用的顺序一致。

To work around the need to reference elements, Cypress has a feature known as aliasing. Aliasing helps you to store and save element references for future use.

Cypress 提供了一种叫做 aliasing 的机制,能将元素引用保存下来,以备将来之用。

看一个例子:

cy.get('.my-selector')
  .as('myElement') // sets the alias,使用 as 命令将 get 返回的元素存储到自定义变量 myElement 中。  .click()/* many more actions */cy.get('@myElement') // re-queries the DOM as before (only if necessary),通过@ 引用自定义变量  .click()

使用 then 来对前一个命令 yield 的目标进行操作

cy
  // Find the el with id 'some-link'  .get('#some-link')

  .then(($myElement) => {
    // ...massage the subject with some arbitrary code
    // grab its href property    const href = $myElement.prop('href')

    // strip out the 'hash' character and everything after it    return href.replace(/(#.*)/, '')
  })
  .then((href) => {
    // href is now the new subject    // which we can work with now  })

Cypress 的异步执行特性

It is very important to understand that Cypress commands don't do anything at the moment they are invoked, but rather enqueue themselves to be run later. This is what we mean when we say Cypress commands are asynchronous.

it('changes the URL when "awesome" is clicked', () => {
  cy.visit('/my/resource/path') // Nothing happens yet
  cy.get('.awesome-selector') // Still nothing happening    .click() // Nope, nothing
  cy.url() // Nothing to see, yet    .should('include', '/my/resource/path#awesomeness') // Nada.})// Ok, the test function has finished executing...// We've queued all of these commands and now// Cypress will begin running them in order!

Cypress doesn't kick off the browser automation magic until the test function exits.

这是 Cypress 不同于其他前端自动测试框架的特别之处:直到测试函数退出,Cypress 才会触发浏览器的自动执行逻辑。

it('does not work as we expect', () => {
  cy.visit('/my/resource/path') // Nothing happens yet
  cy.get('.awesome-selector') // Still nothing happening    .click() // Nope, nothing
  // Cypress.$ is synchronous, so evaluates immediately  // there is no element to find yet because  // the cy.visit() was only queued to visit  // and did not actually visit the application  let el = Cypress.$('.new-el') // evaluates immediately as []
  if (el.length) {
    // evaluates immediately as 0    cy.get('.another-selector')
  } else {
    // this will always run    // because the 'el.length' is 0    // when the code executes    cy.get('.optional-selector')
  }})// Ok, the test function has finished executing...// We've queued all of these commands and now// Cypress will begin running them in order!

正确的做法,把 html 元素 evaluation 的代码放在 then 的callback里:

Each Cypress command (and chain of commands) returns immediately

每个 Cypress 命令(包含命令链)调用后立即返回,不会阻塞住以达到同步运行的效果。

Having only been appended to a queue of commands to be executed at a later time.

这些 command 只是被添加到一个命令队列里,等待 Cypress 框架稍后统一调度执行。

You purposefully cannot do anything useful with the return value from a command. Commands are enqueued and managed entirely behind the scenes.

对于 Cypress 直接返回的命令的执行结果,我们无法对其实行任何有效的操作,因为代码里命令的调用,实际上只是加入到待执行队列里。至于何时执行,由 Cypress 统一调度,对Cypress 测试开发人员来说是黑盒子。

We've designed our API this way because the DOM is a highly mutable object that constantly goes stale. For Cypress to prevent flake, and know when to proceed, we manage commands in a highly controlled deterministic way.

Cypress API 如此设计的原因是,DOM 是一种易变对象,随着用户操作或者交互,状态经常会 go stale. 为了避免出现 flake 情形,Cypress 遵循了上文描述的思路,以一种高度可控,确定性的方式来管理命令执行。

下面一个例子:网页显示随机数,当随机数跳到数字 7 时,让测试停下来。 如果随机数不是数字 7,重新加载页面,继续测试。

下列是错误的 Cypress 代码,会导致浏览器崩溃:

let found7 = falsewhile (!found7) {
  // this schedules an infinite number  // of "cy.get..." commands, eventually crashing  // before any of them have a chance to run  // and set found7 to true  cy.get('#result')
    .should('not.be.empty')
    .invoke('text')
    .then(parseInt)
    .then((number) => {
      if (number === 7) {
        found7 = true
        cy.log('lucky **7**')
      } else {
        cy.reload()
      }
    })}

原因就是:在 while 循环里迅速将巨量的 get command 插入到任务队列(准确的说是 test chain)里,而根本没有机会得到执行。

The above test keeps adding more cy.get('#result') commands to the test chain without executing any!

上面的代码,起到的效果就是,在 while 循环里,不断地将 cy.get 命令,加入到 test chain里,但是任何一个命令,都不会有得到执行的机会!

The chain of commands keeps growing, but never executes - since the test function never finishes running.

命令队列里的元素个数持续增长,但是永远得不到执行的机会,因为 Cypress 代码本身一直在 while 循环里,没有执行完毕。

The while loop never allows Cypress to start executing even the very first cy.get(...) command.

即使是任务队列里第一个 cy.get 语句,因为 while 循环,也得不到执行的机会。

正确的写法:

  1. 利用递归

  2. 在 callback 里书写找到 7 之后 return 的逻辑。

const checkAndReload = () => {
  // get the element's text, convert into a number  cy.get('#result')
    .should('not.be.empty')
    .invoke('text')
    .then(parseInt)
    .then((number) => {
      // if the expected number is found      // stop adding any more commands      if (number === 7) {
        cy.log('lucky **7**')

        return
      }

      // otherwise insert more Cypress commands      // by calling the function after reload      cy.wait(500, { log: false })
      cy.reload()
      checkAndReload()
    })}cy.visit('public/index.html')checkAndReload()

command 执行过程中背后发生的事情

下列这段代码,包含了 5 部分逻辑:

it('changes the URL when "awesome" is clicked', () => {
  cy.visit('/my/resource/path') // 1.
  cy.get('.awesome-selector') // 2.    .click() // 3.
  cy.url() // 4.    .should('include', '/my/resource/path#awesomeness') // 5.})

5 个 步骤的例子:

  1. Visit a URL.

  2. Find an element by its selector.

  3. Perform a click action on that element.

  4. Grab the URL.

  5. Assert the URL to include a specific string.

上述 5 步骤 是 串行执行的,而不是并发执行。每个步骤背后,Cypress 框架都悄悄执行了一些“魔法”:

  1. Visit a URL

魔法:Cypress wait for the page load event to fire after all external resources have loaded

该命令执行时,Cypress 等待页面所有外部资源加载,然后页面抛出 page load 事件。

  1. Find an element by its selector 魔法:如果 find 命令没找到 DOM element,就执行重试机制,直到找到位置。

  2. Perform a click action on that element 魔法:after we wait for the element to reach an actionable state

在 点击元素之前,先等待其成为可以点击状态。

每个 cy 命令都有特定的超时时间,记录在文档里: https://docs.cypress.io/guides/references/configuration

Commands are promise

This is the big secret of Cypress: we've taken our favorite pattern for composing JavaScript code, Promises, and built them right into the fabric of Cypress. Above, when we say we're enqueuing actions to be taken later, we could restate that as "adding Promises to a chain of Promises".

Cypress 在 promise 编程模式的基础上,增添了 retry 机制。

下列这段代码:

it('changes the URL when "awesome" is clicked', () => {
  cy.visit('/my/resource/path')

  cy.get('.awesome-selector').click()

  cy.url().should('include', '/my/resource/path#awesomeness')})

翻译成 promise 风格的 JavaScript 代码为:

it('changes the URL when "awesome" is clicked', () => {
  // THIS IS NOT VALID CODE.  // THIS IS JUST FOR DEMONSTRATION.  return cy
    .visit('/my/resource/path')
    .then(() => {
      return cy.get('.awesome-selector')
    })
    .then(($element) => {
      // not analogous      return cy.click($element)
    })
    .then(() => {
      return cy.url()
    })
    .then((url) => {
      expect(url).to.eq('/my/resource/path#awesomeness')
    })})

Without retry-ability, assertions would randomly fail. This would lead to flaky, inconsistent results. This is also why we cannot use new JS features like async / await.

缺少重试机制,后果就是造成 flaky 和不一致的测试结果,这就是 Cypress 没有选择 async / await 的原因。

You can think of Cypress as "queueing" every command. Eventually they'll get run and in the exact order they were used, 100% of the time.

Cypress 的命令执行顺序和其被插入 test chain 队列的顺序完全一致。

How do I create conditional control flow, using if/else? So that if an element does (or doesn't) exist, I choose what to do?

有的开发人员可能会产生疑问,如何编写条件式控制流,比如在 IF / ELSE 分支里,执行不同的测试逻辑?

The problem with this question is that this type of conditional control flow ends up being non-deterministic. This means it's impossible for a script (or robot), to follow it 100% consistently.

事实上,这种条件式的控制逻辑,会使测试流失去确定性(non-deterministic). 这意味着测试脚本挥着机器人,无法 100% 严格按照测试程序去执行。

下列这行代码:

cy.get('button').click().should('have.class', 'active')

翻译成自然语言就是:

After clicking on this , I expect its class to eventually be active.

注意其中的==eventually==.

This above test will pass even if the .active class is applied to the button asynchronously - or after a indeterminate period of time.

Cypress 会不断重试上述的 assertion,直至 .active class 被添加到 button 上,不管是通过异步添加,还是在一段未知长度的时间段后。

What makes Cypress unique from other testing tools is that commands automatically retry their assertions. In fact, they will look "downstream" at what you're expressing and modify their behavior to make your assertions pass.
You should think of assertions as guards.
Use your guards to describe what your application should look like, and Cypress will automatically block, wait, and retry until it reaches that state.

Cypress 命令默认的 assertion 机制

With Cypress, you don't have to assert to have a useful test. Even without assertions, a few lines of Cypress can ensure thousands of lines of code are working properly across the client and server!
This is because many commands have a built in Default Assertion which offer you a high level of guarantee.

很多 cy 命令都有默认的 assertion 机制。

  • cy.visit() expects the page to send text/html content with a 200 status code. 确保 页面发出 text/html 内容后,收到200 的状态码。

  • cy.request() expects the remote server to exist and provide a response. 确保远端系统存在,并且提供响应。

  • cy.contains() expects the element with content to eventually exist in the DOM. 确保制订的 content 最终在 DOM 中存在。

  • cy.get() expects the element to eventually exist in the DOM.

确保请求的 element 最终在 DOM 中存在。

  • .find() also expects the element to eventually exist in the DOM. - 同 cy.get

  • .type() expects the element to eventually be in a typeable state. 确保元素处于可输入状态。

  • .click() expects the element to eventually be in an actionable state. 确保元素处于可点击状态。

  • .its() expects to eventually find a property on the current subject. 确保当前对象上能够找到对应的 property

All DOM based commands automatically wait for their elements to exist in the DOM.

所有基于 DOM 的命令,都会自动阻塞,直至其元素存在于 DOM 树为止。

cy
  // there is a default assertion that this  // button must exist in the DOM before proceeding  .get('button')

  // before issuing the click, this button must be "actionable"  // it cannot be disabled, covered, or hidden from view.  .click()

在执行 click 命令之前,button 必须成为可点击状态,否则 click 命令不会得到执行。可点击状态(actionable),意思是 button 不能是 disabled,covered,或者 hidden 状态。

Cypress 命令自带的超时设置

cy.get('.mobile-nav').should('be.visible').and('contain', 'Home')
  1. Queries for the element .mobile-nav, 然后停顿 4 秒,直至元素出现在 DOM 里。

  2. 再停顿 4 秒,等待元素出现在页面上。

  3. 再等待 4 秒,等待元素包含 home 的 text 属性。

一段测试程序里的所有 Cypress 命令,共享同一个超时值。

(0)

相关推荐

  • Cypress web自动化27-Debugging调试你的代码

    前言 在写脚本的过程中,有时候会遇到一些问题需要慢慢调试找出原因,Cypress 提供了调试的方法,方便我们快速定位到问题 debugger 调试器 你的Cypress测试代码运行在与应用程序相同的运 ...

  • Cypress web自动化19-自定义命令,把登陆当公共方法commands.js

    前言 测试一个web网站的时候,通常需要先登录.要是每个脚本都写一次登录流程,太麻烦了,于是我们会想到写一个公共函数,这样每次去调用函数即可. cypress 里面提供了一个 commands.js ...

  • Cypress web自动化39-.trigger()常用鼠标操作事件

    前言 在web页面上经常遇到的鼠标事件有:鼠标悬停操作,鼠标右键,鼠标长按,拖拽等操作 trigger() trigger 方法用于在 DOM 元素上触发事件 语法使用示例 .trigger(even ...

  • Cypress web自动化30-操作窗口滚动条(scrollTo)

    前言 web页面的操作,有些元素不在窗口上显示,需滑动滚动条才能显示出来,Cypress 可以使用 scrollTo 操作滚动条的位置. 可以根据窗口的位置来滚动,也可以根据屏幕像素或百分比来滚动. ...

  • Cypress web自动化34-cy.exec()执行系统命令

    前言 cypress 提供了执行系统命令的方法 cy.exec() ,这方便在用例之前准备测试数据,和清理测试数据. cy.exec()语法 执行系统命令语法 cy.exec(command) cy. ...

  • Cypress web自动化21-如何在多个tests之间共享cookies

    前言 Cypress 默认每个用例开始之前会清空所有的cookies,保证每个用例的独立性和干净的环境. 但是我们希望在一个js文件下写多个测试用例的时候,希望只调用一次登录, 记住cookies,后 ...

  • Cypress web自动化28-运行器界面调试元素定位和操作

    前言 Cypress提供了一个很好的测试运行器, 它为你提供了一套可视化结构的测试和断言套件, 很快你也会看到命令, 页面事件, 网络请求等. 当你还没熟练掌握元素定位时,在运行器界面点开探测器,会自 ...

  • Cypress学习6- Assertions断言使用(should, expect)

    前言 每个测试用例需要加断言,Cypress里面断言常用的有should, expect 隐式断言 .should() 可以使用.should()给当前用例加断言 should('have.class ...

  • Cypress web自动化37-cy.wrap() 操作 iframe 上的元素

    前言 iframe 是一种常见的 web 页面上遇到的场景,像有些网站的登录就是放到 iframe 里面的. cypress 如何处理 iframe 上的元素呢,cypress 目前没有提供类似 se ...

  • Cypress web自动化18-cypress.json文件配置baseUrl

    前言 当我们测试一个web网站的时候,一般最好设置一个baseUrl地址,这样方便维护. 一旦部署环境发生了改变,就不需要去基本里面去查找,秩序更改cypress.json文件即可 cypress.j ...

  • 【博文连载】Cypress CY7C68013 介绍

    由于现在的ARM大部分都集成了USB接口,而FPGA由于纯逻辑电路(不要拿异构FPGA比如MPSOC等说事),想要用Verilog HDL设计一个USB2.0内核难度相当大,并且还需要一个PHY,因此 ...

  • Cypress web自动化31-request发post请求登录接口

    前言 cypress 不仅可以用浏览器访问web页面,也可以直接发 request 请求访问接口. 在实际工作中,很多时候都需要先登录,如果只是写登录页面的案例,可以直接在web页面操作. 如果是写其 ...

  • Cypress web自动化35-cy.exec()执行python命令操作数据库

    前言 cy.exec()可以执行系统命令,获取到stdout内容,当我们要操作数据库,准备测试数据的时候,通常用python连数据库操作会非常方便. 我们可以先把操作数据库的方法封装到一个py文件,这 ...

  • Cypress web自动化15-Hooks使用方法

    前言 Cypress 提供了 hooks 函数,方便我们在组织测试用例的时候,设置用例的前置操作和后置清理. 类似于 python 的 unittest 里面的 setUp 和 setUpclass ...

  • Cypress web自动化20-跨域问题-a标签超链接

    前言 cypress 上默认访问一个跨域的网页会出现异常: Cypress detected a cross origin error happened on page load A cross or ...

  • Cypress学习2-pycharm写第一个脚本

    前言 Cypress是javascript语言写的,写js脚本可以用pycharm编辑器上直接编写. 以第一个百度页面搜索框为案例编写一个可以运行的脚本 pycharm导入工程 上一篇新建的项目目录在 ...

  • Cypress web自动化38-alert 弹窗

    前言 当页面上出现 alert 弹窗时候,Cypress 自动接受 alert, 运行代码的时候虽然看不到弹窗页面,但是依然可以对文本内容断言 Alert 弹窗 Cypress 自动接受 alert, ...

  • Cypress学习7-连接器connectors

    前言 关于web页面上的选项,通常我们需要断言选项的个数,遍历每个选项的内容. .each() <ul class="connectors-each-ul"> < ...

  • Cypress学习5-table表格元素(别名使用Aliasing)

    前言 页面上有些元素定位路径比较复杂,可以先定位到该元素使用别名,通过这个别名去操作元素,这样看起来简洁一些. .as()使用别名定位元素 table表格上的元素定位使用示例 <table cl ...