Cypress入门

1,118 阅读26分钟

安装Cypress

安装node和yarn

初始化package.json:

$ your/project/path npm init

通过npm以下方式安装Cypress:

$ cd /your/project/path
$ npm install cypress --save-dev
$your/project/path yarn run cypress open

视频

一个真实的测试

实体测试通常包括三个阶段:

  1. 设置应用程序状态(首先将应用程序置于特定状态)
  2. 采取行动(在应用程序中采取措施使其发生更改)
  3. 对结果应用程序状态进行断言(最后检查所产生的应用程序状态)

转换成测试脚本的步骤如下:

  1. 访问网页。
  2. 查询元素。
  3. 与该元素进行交互。
  4. 断言页面上的内容。

编写一个简单的案例:

describe('My First Test', () => {
  it('Gets, types and asserts', () => {
      //访问网页
    cy.visit('https://example.cypress.io')
      //查询元素;与该元素进行交互
    cy.contains('type').click()
    // 断言页面上的内容 '/commands/actions'
    cy.url().should('include', '/commands/actions')
    // Get an input, type into it and verify that the value has been updated
    cy.get('.action-email')
      .type('fake@email.com')
      .should('have.value', 'fake@email.com')
  })
})

这是一个非常干净的测试!我们不必谈论任何事情如何运作,而只是想验证一系列特定的事件和结果

方便调试技巧

image-20210126181007894

设置后,运行报错时可以直接通过点击报错的JS从而打开编辑器代码报错的位置

配置你的项目baseUrl:

如果您考虑周全,您将很快意识到您将需要大量键入此URL,因为每次测试都需要访问应用程序的某些页面。幸运的是Cypress为此提供了一个配置选项打开您的配置文件cypress.json默认情况下,在您的项目目录中),它的开头为空:

{}

让我们添加该baseUrl选项。

{
  "baseUrl": "http://localhost:8080"
}

这将自动使用此baseUrl作为前缀 cy.visit()cy.request()命令。

现在,我们可以访问相对路径,并省略主机名和端口。

describe('The Home Page', () => {
  it('successfully loads', () => {
    cy.visit('/')
  })
})

查询元素:

如果您以前使用过jQuery,则可能习惯于查询以下元素:

$('.my-selector')

在赛普拉斯中,查询元素相同:

cy.get('.my-selector')

实际上,赛普拉斯捆绑了jQuery,并向您公开了许多DOM遍历方法,因此您可以使用已经熟悉的API轻松处理复杂的HTML结构。

////每个方法都等价于jQuery对应的方法。用你所知道的!
cy.get('#main-content')
  .find('.article')
  .children('img[src^="/static"]')
  .first()

您编写的每个测试都将包含元素选择器。为了省去很多麻烦,您应该编写对更改具有弹性的选择器。

通常,我们看到用户遇到针对其元素的问题,原因是:

  • 您的应用程序可能会使用动态类或ID发生变化的
  • 您的选择器摆脱了对CSS样式或JS行为的开发更改

幸运的是,可以避免这两个问题。

  1. 基于CSS不达标的元素属性,如:idclasstag
  2. 不要定位可能会改变其元素的元素 textContent
  3. 添加data-*属性以更轻松地定位元素

这个怎么运作:

给定一个我们要与之交互的按钮:

<button id="main" class="btn btn-large" name="submission"
  role="button" data-cy="submit">Submit</button>

让我们研究如何定位它:

选择器推荐的笔记
cy.get('button').click()决不最糟糕-太通用,没有上下文。
cy.get('.btn.btn-large').click()决不坏。加上造型。高度更改。
cy.get('#main').click()很少更好。但是仍然与样式或JS事件侦听器结合在一起。
cy.get('[name=submission]').click()很少耦合到name具有HTML语义的属性。
cy.contains('Submit').click()依靠好多了。但是仍然与可能改变的文本内容耦合。
cy.get('[data-cy=submit]').click()总是最好。与所有更改隔离。

通过靶向上面的元件tagclass或者id是非常不稳定和高度受变化。您可以换出元素,可以重构CSS并更新ID,或者可以添加或删除影响元素样式的类。

相反,将data-cy属性添加到元素将为我们提供一个仅用于测试的目标选择器。

data-cy属性不会因CSS样式或JS行为改变而改变,这意味着它不会与元素的行为样式耦合。

此外,它使每个人都清楚该元素被测试代码直接使用。

cypress不像jQuery:

**问题:**当jQuery从其选择器中找不到任何匹配的DOM元素时会发生什么?

答: *糟糕!*它返回一个空的jQuery集合。我们有一个真正的对象可以使用,但是它不包含我们想要的元素。因此,我们开始添加条件检查并手动重试查询。

// $() returns immediately with an empty collection.
const $myElement = $('.element').first()

// Leads to ugly conditional checks
// and worse - flaky tests!
if ($myElement.length) {
  doSomething($myElement)
}

**问:**当赛普拉斯从其选择器中找不到任何匹配的DOM元素时,会发生什么?

答: *没什么大不了的!*赛普拉斯会自动重试查询,直到:

1.找到元素

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

2.达到设置的超时

cy
  // cy.get() looks for '#element-does-not-exist', repeating the query until...
  // ...it doesn't find the element before its timeout.
  // Cypress halts and fails the test.
  .get('#element-does-not-exist')

  // ...this code is never run...
  .then(($myElement) => {
    doSomething($myElement)
  })

这使赛普拉斯具有强大的功能,并且不受其他测试工具中出现的许多常见问题的影响。考虑所有可能导致查询DOM元素失败的情况:

  • DOM尚未加载。
  • 您的框架尚未完成引导。
  • XHR请求未响应。
  • 动画尚未完成。
  • 还有……

以前,您将不得不编写自定义代码来防止出现所有这些问题:任意等待,有条件的重试以及空检查等令人讨厌的混搭。不在赛普拉斯!借助内置的重试和可自定义的超时功能,赛普拉斯避开了所有这些棘手的问题。

指挥链

了解赛普拉斯将命令链接在一起的机制非常重要。它代表您管理一个Promise链,每个命令对下一个命令产生一个“主题”,直到链结束或遇到错误为止。开发人员不必直接使用Promises,但了解它们的工作方式将很有帮助!

与元素互动

正如我们在最初的示例中看到的那样,赛普拉斯允许您单击并使用带有or命令的命令.click()并在页面上键入内容。这是行动连锁的一个很好的例子。让我们再看一遍:.type()cy.get()cy.contains()

cy.get('textarea.post-body')
  .type('This is an excellent post.')

我们将链接.type()cy.get(),告诉它键入cy.get()命令产生的主题,该主题将是DOM元素。

赛普拉斯提供了与您的应用程序交互的更多操作命令:

这些命令保证一些担保什么元素的状态应该是之前执行他们的行动。

例如,在编写.click()命令时,赛普拉斯确保能够与该元素进行交互(就像真实用户一样)。它会通过以下方式自动等待,直到元素达到“可操作”状态:

  • 没有被隐藏
  • 没有被覆盖
  • 没有被禁用
  • 没有动画

在测试中与您的应用程序交互时,这也有助于防止剥落。通常,您可以使用force选项覆盖此行为。

断言元素

断言可让您执行诸如确保元素可见或具有特定属性,CSS类或状态的操作。断言是使您能够描述应用程序所需状态的命令。如果断言没有通过,赛普拉斯将自动等待,直到您的元素达到此状态,否则测试将失败。快速查看实际的断言:

cy.get(':checkbox').should('be.disabled')

cy.get('form').should('have.class', 'form-horizontal')

cy.get('input').should('not.have.value', 'US')

在上述每个示例中,必须注意,赛普拉斯将自动等待这些声明通过。这使您不必知道或关心元素最终确实达到此状态的确切时间。

我们将在本指南的后面部分进一步了解断言

用于.then()对主题采取行动

是否想跳入命令流并直接接触该主题?没问题,.then()在您的命令链中添加一个。上一个命令解析后,它将使用产生的主题作为第一个参数来调用您的回调函数。

如果您希望在命令之后继续链接命令.then(),则需要指定要屈服于这些命令的主题,您可以使用null或返回值来实现该主题undefined。赛普拉斯将为您提供下一条命令。

让我们看一个例子:

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

核心理念

cy.then()在《核心概念指南》中,我们还有更多示例和用例,它们教您如何正确处理异步代码,何时使用变量以及什么是别名。

使用别名引用以前的主题

赛普拉斯具有一些新增功能,可以快速参考过去的主题,称为别名。看起来像这样:

cy
  .get('.my-selector')
  .as('myElement') // sets the alias
  .click()

/* many more actions */

cy
  .get('@myElement') // re-queries the DOM as before (only if necessary)
  .click()

这样,当元素仍在DOM中时,我们便可以将DOM查询重用于更快的测试,并且当在DOM中没有立即找到该元素时,它会自动为我们重新查询DOM。在处理进行大量重新渲染的前端框架时,这特别有用!

命令是异步的

理解Cypress命令在被调用时不执行任何操作,而是让它们自己排队以便稍后运行,这一点非常重要。这就是说赛普拉斯命令是异步的。

以这个简短的测试为例:

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命令与同步代码混合使用,请记住,Cypress命令异步运行非常重要。同步代码将立即执行-无需等待其上方的Cypress命令执行。

不正确的用法

在下面的示例中,在执行el之前,立即求值cy.visit(),因此将始终求值为空数组。

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!

正确用法

以下是可以重写上面的代码以确保命令按预期运行的一种方法。

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
    .then(() => {
      // placing this code inside the .then() ensures
      // it runs after the cypress commands 'execute'
      let el = Cypress.$('.new-el') // evaluates after .then()

      if (el.length) {
        cy.get('.another-selector')
      } else {
        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!

不正确的用法

在下面的示例中,对username值的检查会在cy.visit()执行之前立即得到评估,因此将始终评估为undefined

it('test', () => {
  let username = undefined     // evaluates immediately as undefined

  cy.visit('https://app.com') // Nothing happens yet
  cy.get('.user-name')        // Still, nothing happens yet
    .then(($el) => {          // Nothing happens yet
      // this line evaluates after the .then executes
      username = $el.text()
    })

  // this evaluates before the .then() above
  // so the username is still undefined
  if (username) {             // evaluates immediately as undefined
    cy.contains(username).click()
  } else {
    // this will always run
    // because username will always
    // evaluate to undefined
    cy.contains('My Profile').click()
  }
})

// Ok, the test function has finished executing...
// We've queued all of these commands and now
// Cypress will begin running them in order!

正确用法

以下是可以重写上面的代码以确保命令按预期运行的一种方法。

it('test', () => {
  let username = undefined     // evaluates immediately as undefined

  cy.visit('https://app.com') // Nothing happens yet
  cy.get('.user-name')        // Still, nothing happens yet
    .then(($el) => {          // Nothing happens yet
      // this line evaluates after the .then() executes
      username = $el.text()

      // evaluates after the .then() executes
      // it's the correct value gotten from the $el.text()
      if (username) {
        cy.contains(username).click()
      } else {
        cy.get('My Profile').click()
      }
    })
})

// Ok, the test function has finished executing...
// We've queued all of these commands and now
// Cypress will begin running them in order!

核心理念

每个赛普拉斯命令(和命令链)都立即返回,仅附加到稍后要执行的命令队列中。

您故意无法使用命令的返回值做任何有用的事情。命令完全在后台排队和管理。

我们之所以设计API,是因为DOM是一个高度可变的对象,并且会不断陈旧。为了使赛普拉斯防止剥落并知道何时进行处理,我们以高度受控的确定性方式来管理命令。

为什么我不能使用异步/等待?

如果您是现代JS程序员,您可能会听到“异步”并思考:**为什么我不能只使用async/await**而不是学习一些专有API?

赛普拉斯的API的构建与您通常使用的API截然不同:但是这些设计模式是故意的。我们将在本指南后面详细介绍。

命令顺序运行

测试功能运行完毕后,赛普拉斯开始执行使用cy.*命令链排队的命令。

让我们再来看一个例子

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.
})

上面的测试将按以下顺序执行:

  1. 访问URL。
  2. 通过其选择器查找元素。
  3. 在该元素上执行点击操作。
  4. 抓取URL。
  5. 声明URL以包括特定的字符串

这些动作将始终以串行方式(一个接一个)进行,而不会并行(同时)进行。为什么?

为了说明这一点,让我们重新查看该动作列表,并展示赛普拉斯在每个步骤中为我们所做的一些隐蔽的✨魔术

  1. 访问 URL✨load在所有外部资源加载完毕等待页面事件触发
  2. 查找到其选择的元素 ✨重试,直到它在DOM中发现
  3. 执行元件上的点击动作 ✨**后,我们等待达成元素可操作状态**✨
  4. 抓取网址并…
  5. 声明URL以包括特定字符串 ✨,然后重试直到断言通过

如您所见,赛普拉斯做了很多额外的工作来确保应用程序的状态与我们的命令期望的相符。每个命令可能很快就会解决(很快,您就不会看到它们处于挂起状态),但是其他命令可能要花费几秒钟甚至几十秒钟才能解决。

尽管大多数命令会在几秒钟后超时,但是其他希望特定事情花费更长时间的专门命令cy.visit()自然会在超时之前等待更长的时间。

这些命令具有它们自己的特定超时值,这些值记录在我们的配置中

核心理念

确保成功完成步骤所需的任何等待或重试必须在下一步开始之前完成。如果在超时之前未成功完成测试,则测试将失败。

命令是应许

这是赛普拉斯的最大秘密:我们采用了我们最喜欢的模式来编写JavaScript代码Promises,并将其直接构建到赛普拉斯的结构中。上面,当我们说我们要排队等待以后采取的行动时,我们可以重申为“将承诺添加到承诺链中”。

让我们将之前的示例与基于虚构的,基于Promise的代码的虚构版本进行比较:

嘈杂的承诺演示。无效的代码。

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

赛普拉斯的真实面貌,承诺被包裹起来并向我们隐瞒。

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

巨大差距!赛普拉斯除了阅读内容更清晰外,还可以做更多的事情,因为Promises本身没有可重试性的概念

没有retry-ability,断言将随机失败。这将导致片状,不一致的结果。这也是为什么我们不能使用JS等新JS功能的原因async / await

赛普拉斯无法产生与其他命令隔离开的原始值。这是因为赛普拉斯命令在内部的作用类似于异步数据流,该数据流仅在受到其他命令的影响和修改后才能解析。这意味着我们无法分块地产生离散值,因为在传递值之前,我们必须了解有关您期望的一切。

这些设计模式确保我们可以创建确定性可重复性一致的****无碎片测试。

赛普拉斯是使用Bluebird的Promises构建的。但是,赛普拉斯命令不会返回这些典型的Promise实例。取而代之的是,我们返回所谓的a Chainer,它就像位于内部Promise实例之上的一层。

因此,您永远无法从赛普拉斯命令中返回或分配任何有用的信息。

如果您想了解有关处理异步赛普拉斯命令的更多信息,请阅读我们的《核心概念指南》

命令不是承诺

赛普拉斯API并非Promises的精确1:1实现。它们具有Promise一样的品质,但是您应该意识到一些重要的差异。

  1. 你不能参加比赛或同时运行多个命令(平行)。
  2. 您不能“偶然地”忘记返回或链接命令。
  3. 您不能将.catch错误处理程序添加到失败的命令。

这些限制都内置在赛普拉斯API具体原因。

赛普拉斯的整个目的(以及与其他测试工具的不同之处)是创建一致的,非松散的测试,这些测试从一次运行到另一次运行都具有相同的性能。实现这一目标并非免费-我们需要进行一些折衷,而这些折衷对于习惯于使用Promises的开发人员来说似乎并不熟悉。

让我们深入了解每个权衡:

您不能同时运行或运行多个命令

赛普拉斯保证它将在每次运行时确定且相同地执行所有命令。

许多赛普拉斯命令以某种方式改变了浏览器的状态。

以上命令都不是幂等的;它们都会引起副作用。竞速命令是不可能的,因为命令必须以受控的串行方式运行才能创建一致性。由于集成和端到端测试主要模拟真实用户的行为,因此赛普拉斯在真实用户逐步工作之后对其命令执行模型进行建模。

您不会意外忘记返回或链接命令

实际上,如果不退还或正确链接嵌套的Promise,非常容易。

让我们想象一下以下Node代码:

// assuming we've promisified our fs module
return fs.readFile('/foo.txt', 'utf8')
.then((txt) => {
  // oops we forgot to chain / return this Promise
  // so it essentially becomes 'lost'.
  // this can create bizarre race conditions and
  // bugs that are difficult to track down
  fs.writeFile('/foo.txt', txt.replace('foo', 'bar'))

  return fs.readFile('/bar.json')
  .then((json) => {
    // ...
  })
})

在Promise世界中甚至可以做到这一点的原因是,您有能力并行执行多个异步操作。在后台,每个promise“链”都返回一个promise实例,该实例跟踪链接的父实例和子实例之间的关系。

由于赛普拉斯强制命令只能串行运行,因此您无需在赛普拉斯中对此加以关注。我们将所有命令排队到全局单例中。因为只有一个命令队列实例,所以命令永远不会*“丢失”*。

您可以将赛普拉斯视为“排队”每个命令。最终,它们将开始运行,并完全按照使用的顺序(100%的时间)被使用。

无需使用return赛普拉斯命令。

您不能将.catch错误处理程序添加到失败的命令

在赛普拉斯中,无法从失败的命令中恢复内置错误。一条命令及其断言最终都会全部通过,或者如果一条命令失败,则所有其余命令均不会运行,并且测试将失败。

您可能想知道:

如何使用if / else创建条件控制流?这样,如果某个元素存在(或不存在),我会选择做什么?

这个问题的问题在于,这种类型的条件控制流最终是不确定的。这意味着脚本(或机器人)不可能始终如一地遵循它。

通常,只有少数几种非常特殊的情况可以创建控制流。要求从错误中恢复实际上与要求另一个if/else控制流相同。

话虽如此,只要您知道控制流的潜在陷阱,就有可能在赛普拉斯中做到这一点!

您可以在此处阅读有关如何进行条件测试的所有信息。

断言

正如我们之前在本指南中提到的:

断言描述元素对象应用程序期望状态。

赛普拉斯与其他测试工具的不同之处在于,命令会自动重试其断言。实际上,他们将把您所表达的内容“下游”看待,并修改其行为以使您的主张通过。

您应该将断言视为警卫

使用防护措施来描述应用程序的外观,赛普拉斯将自动**阻止,等待并重试,**直到达到该状态为止。

核心理念

每个API命令都使用断言来记录其行为-例如它如何重试或等待断言通过。

用英语断言

让我们看看您如何用英语描述断言:

单击此<button>按钮后,我希望它的课程最终是active

为了在赛普拉斯中表达这一点,您需要编写:

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

即使将该.active类异步应用于按钮-或经过一段不确定的时间后,上述测试也会通过。

// even though we are adding the class
// after two seconds...
// this test will still pass!
$('button').on('click', (e) => {
  setTimeout(() => {
    $(e.target).addClass('active')
  }, 2000)
})

这是另一个例子。

向服务器发出HTTP请求后,我希望响应主体等于 {name: 'Jane'}

要用断言来表达这一点,您可以编写:

cy.request('/users/1').its('body').should('deep.eq', { name: 'Jane' })

什么时候断言?

尽管赛普拉斯向您提供了数十个断言,但有时最好的测试可能根本没有断言!怎么会这样?断言不是测试的基本部分吗?

考虑以下示例:

cy.visit('/home')

cy.get('.main-menu')
  .contains('New Project')
  .click()

cy.get('.title')
  .type('My Awesome Project')

cy.get('form')
  .submit()

如果没有一个明确的断言,该测试就会以多种方式失败!这里有一些:

  • 最初的人cy.visit()可能会以成功以外的方式做出回应。
  • 任何cy.get()命令都可能无法在DOM中找到其元素。
  • 我们想要的元素.click()可以被另一个元素覆盖。
  • 我们想要输入的内容.type()可能被禁用。
  • 提交表单可能会导致状态代码失败。
  • 页内JS(正在测试的应用程序)可能会引发错误。

您还能想到更多吗?

核心理念

使用赛普拉斯,您不必断言要进行有用的测试。即使没有断言,赛普拉斯的几行代码也可以确保数千行代码在客户端和服务器上正常工作!

这是因为许多命令具有内置的默认断言,可为您提供高水平的保证。

默认断言

许多命令具有默认的内置断言,或者具有一些可能导致其失败而无需添加显式断言的要求。

例如:

  • cy.visit()希望页面发送text/html带有200状态码的内容。
  • cy.request() 希望远程服务器存在并提供响应。
  • cy.contains() 希望具有内容的元素最终存在于DOM中。
  • cy.get() 期望元素最终存在于DOM中。
  • .find() 还希望该元素最终存在于DOM中。
  • .type()期望元素最终处于可键入状态。
  • .click()期望元素最终处于可操作状态。
  • .its() 期望最终找到有关当前主题的属性。

某些命令可能有特定要求,导致它们立即失败而无需重试:例如cy.request()

其他命令(例如,基于DOM的命令)将自动重试,并在失败之前等待其相应元素存在。

甚至更多-操作命令将在失败之前自动等待其元素达到可操作状态

核心理念

所有基于DOM的命令都会自动等待其元素出现在DOM中。

永远需要编写.should('exist')一个基于DOM的命令后。

大多数命令使您能够灵活地覆盖或绕过它们失败的默认方式,通常通过传递一个{force: true}选项来实现。

示例#1:存在性和可操作性

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()

赛普拉斯将自动等待元素传递其默认断言。就像您添加的显式断言一样,所有这些断言共享相同的超时值。

示例#2:反转默认断言

大多数情况下,查询元素时,您希望它们最终存在。但是有时您希望等到它们存在。

您所要做的就是添加断言,赛普拉斯将逆转其规则,等待元素存在。

// now Cypress will wait until this
// <button> is not in the DOM after the click
cy.get('button.close').click().should('not.exist')

// and now make sure this #modal does not exist in the DOM
// and automatically wait until it's gone!
cy.get('#modal').should('not.exist')

核心理念

通过添加.should('not.exist')到任何DOM命令,赛普拉斯将反转其默认断言,并自动等待直到该元素不存在。

示例3:其他默认断言

其他命令具有与DOM不相关的其他默认断言。

例如,.its()要求您要查询的属性存在于对象上。

// create an empty object
const obj = {}

// set the 'foo' property after 1 second
setTimeout(() => {
  obj.foo = 'bar'
}, 1000)

// .its() will wait until the 'foo' property is on the object
cy.wrap(obj).its('foo')

断言清单

赛普拉斯将,和捆绑在一起ChaiChai-jQuerySinon-Chai提供内置的断言。您可以在断言参考列表中看到它们的完整列表。您也可以将自己的断言编写为Chai插件,并在Cypress中使用它们。

写作断言

在赛普拉斯中有两种写断言的方法:

  1. **隐含主题:**使用.should().and()
  2. **显式主题:**使用expect

隐含主体

使用.should().and()命令是在赛普拉斯中进行断言的首选方法。这些是典型的赛普拉斯命令,这意味着它们适用于命令链中当前产生的主题。

// the implicit subject here is the first <tr>
// this asserts that the <tr> has an .active class
cy.get('tbody tr:first').should('have.class', 'active')

您可以使用来将多个断言链接在一起.and(),这是另一个名称,.should()可以使事情更易读:

cy.get('#header a')
  .should('have.class', 'active')
  .and('have.attr', 'href', '/users')

因为.should('have.class')不改变主题,.and('have.attr')所以针对同一元素执行。当您需要针对单个主题快速声明多个内容时,这非常方便。

如果我们以“很长的路要走”的明确形式写这个断言,它看起来像这样:

cy.get('tbody tr:first').should(($tr) => {
  expect($tr).to.have.class('active')
  expect($tr).to.have.attr('href', '/users')
})

隐式形式要短得多!那么您什么时候要使用显式形式?

通常,当您想要:

  • 断言关于同一主题的多件事
  • 在断言之前以某种方式对对象进行按摩

显式主题

使用expect允许您传递特定主题并对其进行断言。这可能是您习惯于看到在单元测试中编写的断言的方式:

// the explicit subject here is the boolean: true
expect(true).to.be.true

您知道您可以在赛普拉斯中编写单元测试吗?

查看我们用于单元测试单元测试React组件的示例配方。

当您要执行以下操作时,显式断言非常有用:

  • 在进行断言之前,请执行自定义逻辑。
  • 针对同一主题做出多个断言。

.should()命令允许我们传递一个回调函数,该函数将产生的主题作为第一个参数。它的工作原理类似于.then(),除了Cypress自动等待并重试回调函数内部的所有内容以使其通过。

复杂断言

下面的示例是一个用例,其中我们在多个元素之间进行断言。使用.should()回调函数是一种从父级查询多个子级元素并声明其状态的好方法。

这样做可以使您确保后代的状态与您的期望相符,从而无需使用常规赛普拉斯DOM命令单独查询后代即可阻止保护赛普拉斯。

cy
  .get('p')
  .should(($p) => {
    // massage our subject from a DOM element
    // into an array of texts from all of the p's
    let texts = $p.map((i, el) => {
      return Cypress.$(el).text()
    })

    // jQuery map returns jQuery object
    // and .get() converts this to an array
    texts = texts.get()

    // array should have length of 3
    expect(texts).to.have.length(3)

    // with this specific content
    expect(texts).to.deep.eq([
      'Some text from first p',
      'More text from second p',
      'And even more text from third p'
    ])
  })

确保.should()安全

当使用带有的回调函数时.should(),请确保整个函数可以多次执行而没有副作用。赛普拉斯将其重试逻辑应用于这些功能:如果发生故障,它将重复运行这些断言,直到达到超时为止。这意味着您的代码应重试安全。这个技术术语意味着您的代码必须是幂等的

超时时间

几乎所有命令都可能以某种方式超时。

所有断言,无论它们是默认断言还是您添加的断言都共享相同的超时值。

应用超时

您可以修改命令的超时。此超时会影响其默认断言(如果有)以及您添加的任何特定断言。

请记住,因为断言用于描述先前命令的条件-timeout修改是对先前命令而不是断言进行

示例1:默认断言

// because .get() has a default assertion
// that this element exists, it can time out and fail
cy.get('.mobile-nav')

赛普拉斯的引擎盖下:

  • 查询元素.mobile-nav,最多等待4秒使其在DOM in中存在

示例2:其他断言

// we've added 2 assertions to our test
cy
  .get('.mobile-nav')
  .should('be.visible')
  .and('contain', 'Home')

赛普拉斯的引擎盖下:

  • 查询的元素.mobile-nav并等待长达4秒为它在DOM存在✨ ✨并等待长达4秒使其可见✨ ✨并等待长达4秒为它包含文本:“主页”

赛普拉斯将等待的时间量的所有断言的传递是的持续时间(其为4秒)。cy.get() timeout

可以按命令修改超时,这将影响所有默认断言以及该命令后链接的所有断言。

Example#3:修改超时

// we've modified the timeout which affects default + added assertions
cy
  .get('.mobile-nav', { timeout: 10000 })
  .should('be.visible')
  .and('contain', 'Home')

赛普拉斯的引擎盖下:

  • 获取元素.mobile-nav并最多等待10秒钟,它在DOM存在✨ ✨并最多等待10秒使其可见✨ ✨并等待长达10秒为它包含文本:“主页”

请注意,此超时已经涉及所有断言,并且赛普拉斯现在将等待总计10秒以使所有断言通过。

默认值

赛普拉斯根据命令类型提供几种不同的超时值。

我们已根据期望某些操作采取的时间设置了它们的默认超时时间。

例如:

  • cy.visit()加载一个远程页面,直到所有外部资源都完成其加载阶段后,它才能解析。这可能需要一段时间,因此其默认超时设置为60000ms
  • cy.exec()运行系统命令,例如播种数据库。我们预计这可能会花费很长时间,并且其默认超时设置为60000ms
  • cy.wait()实际上使用2个不同的超时时间。等待路由别名时,我们等待的匹配请求5000ms,然后等待服务器的响应30000ms。我们希望您的应用程序能够快速发出匹配请求,但是我们希望服务器的响应时间可能更长。

这使得大多数其他命令(包括所有基于DOM的命令)默认在4000毫秒后超时。

技巧API

1.获取元素文本

安装插件cypress-commands

npm install cypress-commands

将cypress-commands导入项目中 cypress/support/index.js

import 'cypress-commands'

.text() 正确用法

<div>Catastrophic Cat</div>
<div>Dramatic Dog</div>
<div>Amazing Ant</div>
// yields [
//   "Catastrophic Cat",
//   "Dramatic Dog",
//   "Amazing Ant"
// ]
cy.get('div').text();

2.等待特定的请求响应

//等待别名为“getAccount”的路由响应
//而不改变或打断它的反应
cy.visit('/accounts/123')
cy.wait('@getAccount').then((interception) => {
//我们现在可以进入低层拦截
//包含请求主体的,
//回应主体、状态等
}
赛普拉斯会自动等待网络调用完成,然后再执行下一个命令。

// 反模式:在内部放置Cypress命令,然后回调
cy.wait('@alias')
  .then(() => {
    cy.get(...)
  })

// 推荐做法:按顺序编写Cypress命令
cy.wait('@alias')
cy.get(...)

// 示例:assert status from截距()继续之前
cy.wait('@alias').its('response.statusCode').should('eq', 200)
cy.get(...)

3.共享登录

测试脚本:

/// <reference types="cypress" />
describe('My First Test', () => {
  beforeEach(() => {
    cy.login("XXXX", "a123456")
  })
  it('login1', () => {
    cy.wait(5000)
    cy.visit('/guzhenhuazZ')
    cy.contains('个人设置').should('exist')
  })
  it('login2', () => {
    cy.intercept( '*/unread_count').as('unread_count')
    // this 'cy.wait' will only resolve once a request is made to '/search'
    // with the query paramater 'q=some+terms'
    cy.wait('@unread_count')
    //访问网页
    cy.visit('/guzhenhuazZ')
    cy.contains('个人设置').should('exist')
    
  })
})

support/commands.js

Cypress.Commands.add('login', (name, password) => {
    cy.visit('/login')
    cy.get('#user_login').type(name)
    cy.get('#user_password').type(password)
    cy.get(':nth-child(4) > .ui').click()
})

4.junit-allure报告

在cypress run的执行过程中,每一个测试用例文件都是完全单独运行的。执行完用例后可以生产对应的报告文件,再结合 allure 可以生成 allure 的报告。

在 cypress.json 中加入如下配置

// 作者:上海-悠悠 交流QQ群:939110556
// 原文blog: https://www.cnblogs.com/yoyoketang
{

  "reporter": "junit",
  "reporterOptions": {
    "mochaFile": "results/test_report_[hash].xml", 
    "toConsole": true
  }
}

从Cypress 3+开始,在cypress run的执行过程中,每一个测试用例文件都是完全单独运行的,这意味着后面的测试结果会覆盖之前的测试结果呢。 为了针对每个测试文件生成单独的测试报告,请在mochaFile文件中使用[hash]:

"mochaFile": "results/test_report_[hash].xml"

也可以通过命令行传对应的参数

cypress run --reporter junit --reporter-options "mochaFile=results/test_report_[hash].xml,toConsole=true"

运行用例

通过cypress run 运行测试用例

cypress run --browser chrome

用例运行后会在report目录下生成xml报告

img

allure命令行工具

allure是一个命令行工具,需要去github上下载最新版github.com/allure-fram…

img

下载完成之后,解压到本地电脑

img

img

把bin目录添加到环境变量Path下

img

allure报告

cd到cypress 项目根目录执行

allure serve results

生成 allure 测试报告

img

5.调用API

6.连接数据库