八、理解 Cypress 中的变量和别名

在我们开始了解变量和别名如何在 Cypress 中工作之前,了解我们在前面几章中介绍的内容非常重要,这些内容涉及如何在 Cypress 中编写测试,如何配置测试,甚至如何使用 Cypress 按照测试驱动的开发方法以正确的方式编写应用。在我们深入研究变量和别名如何工作时,本书前几章中提供的背景信息将为您提供良好的基础。通过探索什么是变量和别名,我们将了解如何在 Cypress 中创建引用,这将简化我们的测试编写过程和测试的复杂性。了解如何使用变量和别名不仅能让你写出更好的测试,还能写出易于阅读和维护的测试。

在本章中,我们将着重于编写异步命令,以利用 Cypress 附带的变量和别名。我们还将了解如何通过使用别名来简化我们的测试,以及如何利用别名和我们在测试的不同领域创建的变量,例如对元素、路由和请求的引用。

我们将在本章中讨论以下关键主题:

  • 了解 Cypress 变量
  • 了解 Cypress 别名

一旦您完成了这些主题中的每一个,您将完全理解如何在您的 Cypress 测试中使用别名和变量。

技术要求

首先,我们建议您从 GitHub 中克隆包含源代码和我们将在本章中编写的所有测试的存储库。

本章的 GitHub 资源库可在https://GitHub . com/packt publishing/端到端-Web-Testing-with-Cypress 上找到。

本章的源代码可以在chapter-08目录中找到。

理解 Cypress 变量

本节将重点介绍 Cypress 中有哪些变量,它们在测试中是如何使用的,以及它们在测试中的作用,尤其是在降低测试复杂性方面。我们还将探索不同的领域,在这些领域中,我们可以使用 Cypress 变量来增加测试的可读性。到本节结束时,您将能够使用变量编写测试,并且还能理解在编写测试时应该在哪里使用变量。

为了更好地理解 Cypress 中的变量是如何工作的,了解 Cypress 如何执行其命令是很重要的。下面的代码块是一个测试,首先选择一个按钮,然后选择一个输入元素,然后单击该按钮:

it('carries out asynchronous events', () => {
   const button = cy.get('#submit-button');
   const username = cy.get('#username-input');
   button.click()
});

前面的代码块说明了一个测试,该测试应该首先识别按钮,然后识别用户名输入,最后单击按钮。然而,测试和执行不会以我们通常假设的方式发生。在我们的假设中,我们可能认为第一个命令将在第二个命令运行之前执行并返回结果,然后第三个命令将是最后一个执行的命令。Cypress 利用 JavaScript 异步 API,控制命令在 Cypress 测试中的执行方式。

重要说明

异步应用编程接口是这样实现的,它们在接收到命令或请求时提供响应,并且在处理其他请求之前不必等待一个特定的请求得到响应。相反,应用编程接口返回收到的第一个响应,并继续执行尚未收到的响应。发出请求和接收响应的非阻塞机制确保了可以同时发出不同的请求,因此使我们的应用看起来是多线程的,而实际上,它本质上是单线程的。

在前面的代码 块中,Cypress 以异步顺序执行命令,其中响应不一定按照我们测试中请求的顺序返回。然而,我们可以强制 Cypress 按照我们的意愿执行我们的测试,我们将在下一节探讨 Cypress 的闭包时讨论这一点。

关闭

当 Cypress 将测试函数和对函数周围状态的引用捆绑在一起时,就创建了闭包。闭包是一个 JavaScript 概念,Cypress 大量借鉴了这个概念。因此,Cypress 的测试闭包可以访问我们测试的外部范围,也可以访问它的内部范围,这将由测试函数创建。我们将测试的本地功能范围称为词法环境,就像 JavaScript 函数中的情况一样。在下面的代码块中,我们可以看到 Cypress 中有哪些闭包,以及变量是如何在闭包中使用的:

describe('Closures', () => {
    it('creates a closure', () => {
       // { This is the external environment for the test }
      cy.get('#submit-button').then(($submitBtn) => {
       // $submitBtn is the Object of the yielded cy.get()
       // response
       // { This is the lexical environment for the test }
      })
     // Code written here will not execute until .then()  
      //finishes execution
    })
  });

创建$submitBtn变量是为了访问从cy.get('#submit-button')命令获得的响应。使用我们刚刚在测试中创建的变量,我们可以访问返回的值并与之交互,就像在普通函数中一样。在这个测试中,我们使用$submitBtn变量创建了一个测试结束函数。.then()函数创建一个回调函数,使我们能够在代码块中嵌套其他命令。闭包的优势在于我们可以控制我们的测试如何执行命令。在我们的测试中,我们可以等到.then()方法中的所有嵌套命令执行完毕后,再运行测试中的任何其他命令。我们在测试代码中的注释进一步描述了执行行为。

重要说明

回调函数是在其他函数内部作为参数传递的函数,然后在外部函数内部调用以完成一个动作。当我们的.then()函数内的命令完成运行时,函数外的其他命令将继续它们的执行例程。

在下面的代码块中,我们将探索如何通过使用变量并确保闭包内的代码在闭包外的任何其他代码开始执行之前和之后首先执行来编写测试。测试将添加两个待办事项,但是在添加第二个待办事项之前,我们将使用闭包来验证闭包中的代码是否首先被执行:

it('can Add todo item - (Closures)', () => {
      cy.visit('http://todomvc.com/examples/react/#/')
      cy.get(".new-todo").type("New Todo {Enter}");
      cy.get('.todo-list>li:nth-child(1)').then(($todoItem) => {
        // Storing our todo item Name 
        const txt = $todoItem.text()
        expect(txt).to.eq('New Todo')
      });
      // This command will run after all the above commands  
      // have finished their execution.  
      cy.get(".new-todo").type("Another New Todo {Enter}");
    });

在前面的代码块中,我们已经向我们的待办事项列表中添加了一个待办事项,但是在添加第二个事项之前,我们验证了添加的待办事项确实是我们创建的。为了实现这一点,我们使用了一个闭包和一个回调函数,该函数需要在我们执行下一个命令之前返回true。下面的截图显示了我们运行测试的执行步骤:

Figure 8.1 – Closures in Cypress

图 8.1–Cypress 的封闭

图 8.1 中,我们可以看到 Cypress 执行了获取添加的待办事项的命令,并断言添加的待办事项是我们在执行最后一个命令向我们的待办事项列表添加新的待办事项之前在我们的列表中拥有的。

Cypress 的闭包不能存在于变量之外。要使用闭包,我们需要利用变量将从我们的命令接收到的值传递给闭包函数,而利用变量是唯一的方法。在这个代码块中,我们使用了一个$todoItem变量将cy.get()命令的值传递给闭包,该闭包断言找到的待办事项正是我们创建的事项。

Cypress 利用变量范围,就像在 JavaScript 中一样。在 Cypress 中,用户可以选择使用constvarlet标识符来指定变量声明的范围。在接下来的部分中,我们将看到可以在测试中使用的不同范围。

定义变量

var关键字用于声明函数或全局范围的变量。出于初始化的目的,向变量提供值是可选的。在测试函数中遇到任何其他代码之前,先执行用var关键字声明的变量。可以用var关键字在全局范围内声明一个变量,并在我们测试函数的函数范围内覆盖它。下面的代码块显示了用var关键字声明的全局变量的简单重写:

describe('Cypress Variables', () => {
  var a = 20;
  it('var scope context', () => {
    a = 30; // overriding global scope
    expect(a).to.eq(30) // a = 30
  });
 it('var scope context - changed context', () => {
    // Variable scope remains the same as the change affects 
    // the global scope     expect(a).to.eq(30) //a = 30
  });
});

在这个代码块中,我们在测试的全局上下文中声明了一个a变量,然后在我们的测试中更改了这个全局变量。新更改的变量将成为我们的全局a变量的新值,除非它被显式更改,就像我们在测试中所做的那样。var关键字因此改变了变量的全局上下文,因为它全局地重新分配了全局变量的值。

let变量声明的工作方式与var声明的变量相同,只是定义的变量只能在声明它们的范围内可用。是的,我知道这听起来令人困惑!在下面的代码块中,两个测试显示了使用let关键字时范围声明的差异:

describe('Cypress Variables', () => {
  // Variable declaration
  let a = 20;
  it('let scope context', () => {
    let a = 30;
    // Local scoped variable
    expect(a).to.eq(30) // a = 30
  });
  it('let scope context - global', () => {
    // Global scoped variable
    expect(a).to.eq(30) // a = 20
  });

在第二个测试中,我们有一个测试失败,因为let关键字将只使改变的a变量对改变它的特定测试可用,而不是对我们测试套件的整个全局范围可用,就像var变量声明的情况一样。在下面的截图中,我们可以看到测试失败了,因为它只选择了describe块中声明的变量,而不是前面测试中声明的变量:

Figure 8.2 – The let keyword

图 8.2–字母关键字

如图图 8.2 所示,在编写测试时,可以在不同的测试中对同一个变量进行声明,而不影响声明变量的范围,因为每一个都将属于并有自己的上下文,不会影响全局上下文。

常数

const关键字用于声明只读的对象和变量,一旦声明就不能更改或重新分配。用const关键字赋值的变量是“最终的”,只能在它们所处的状态下使用,而其值不会发生变化。在下面的代码块中,我们试图重新分配用const关键字声明的变量,这将导致失败:

describe('const Keyword', () => {
    const a = 20;
    it('let scope context', () => {
      a = 30;
      // Fails as We cannot reassign
      // a variable declared with a const keyword
      expect(a).to.eq(30) // a = 20
    });
});

从这个代码块中,假设a变量是用const声明的,它是不可变的,因此 Cypress 将失败并出现错误,如下图所示:

Figure 8.3 – The const keyword

图 8.3–常量关键字

就像在 JavaScript 中一样,Cypress 不能重新分配已经用const关键字声明的变量。使用const声明的变量是那些在程序执行期间不需要改变的变量,无论是在测试中的全局还是局部。

回顾–了解 Cypress 变量

在本节中,我们学习了 Cypress 中变量的使用。我们看了变量是如何在闭包中使用的,以及它们是如何用不同的范围和上下文声明的。在这里,我们也了解了变量作用域的含义以及它们如何在测试中使用。既然我们知道了变量是什么以及它们代表什么,我们将在下一节深入探讨别名在 Cypress 测试中的使用。

了解 Cypress 别名

别名是防止在我们的测试中使用.then()回调函数的一种方式。我们使用别名来创建引用或 Cypress 可以引用的某种“内存”,因此减少了我们再次重新声明项目的需要。别名的一个常见用法是避免在我们的beforebeforeEach测试钩子中使用回调函数。别名提供了一种“干净”的方式来访问变量的全局状态,而不需要在每次测试中都调用或初始化变量。在本节中,我们将学习如何在测试执行中正确使用别名,以及建议使用别名的不同场景。

别名在测试套件中的多个测试使用一个变量的情况下非常有用。下面的代码块显示了一个测试,在该测试中,我们希望在将待办事项添加到待办事项列表后验证它是否存在:

context('TODO MVC - Aliases Tests', () => {
  let text;
  beforeEach(() => {
    cy.visit('http://todomvc.com/examples/react/#/')
    cy.get(".new-todo").type("New Todo {Enter}");
    cy.get('.todo-list>li:nth-child(1)').then(($todoItem) => {
      text = $todoItem.text()
    });
  });
  it('gets added todo item', () => {
    // todo item text is available for use
    expect(text).to.eq('New Todo')
  });
});

为了在外部使用在beforeEachbefore钩子中声明的变量,我们在代码块中使用了一个回调函数来访问该变量,然后断言由我们的beforeEach方法创建的变量的文本与我们期望的待办事项相同。

重要说明

代码的结构仅用于演示目的,不建议在编写测试时使用。

虽然前面的测试肯定会通过,但这是 Cypress 别名要解决的一个反模式。Cypress 的别名在 Cypress 测试中用于以下目的:

  • 在钩子和测试之间共享对象上下文
  • 访问 DOM 中的元素引用
  • 访问路线参考
  • 访问请求引用

我们将研究别名的每一种用途,并查看它们如何在涵盖的用途中使用的示例。

在测试钩子和测试之间共享上下文

别名可以提供一种“干净”的方式定义变量,并使它们可以被测试访问,而不需要在我们的测试钩子中使用回调函数,如前面的代码块所示。要创建别名,我们只需将.as()命令添加到我们共享的内容中,然后就可以使用this.*命令从摩卡的上下文对象中访问共享的元素。每个测试的上下文在测试运行后被清除,我们的测试在不同的测试钩子中创建的属性也是如此。下面的代码块显示了与前一个相同的测试,用于检查待办事项是否存在,但这次使用了别名:

describe('Sharing Context between hooks and tests', () => {
    beforeEach(() => {
      cy.visit('http://todomvc.com/examples/react/#/');
      cy.get(".new-todo").type("New Todo {Enter}");
      cy.get('.todo-list>li:nth-
        child(1)').invoke('text').as('todoItem');
    });
    it('gets added todo item', function () {
      // todo item text is available for use
      expect(this.todoItem).to.eq('New Todo');
    });
  });

在前面的代码块中,我们可以验证摩卡的上下文中有this.todoItem并且运行成功,验证待办事项确实被创建了。如下面的截图所示,可以对测试进行进一步的验证,该截图突出显示了在使用别名引用我们的待办事项列表中创建的待办事项后,Cypress 测试的通过状态:

Figure 8.4 – Context sharing

图 8.4–上下文共享

图 8.4 中,我们看到 Cypress 突出显示了别名文本,并显示了它在我们的测试中是如何被调用的。Cypress 打印出已经使用过的别名元素和命令,使得在出现故障时可以很容易地识别和调试,并在别名元素中追踪导致故障的原因。

重要说明

在你的 Cypress 测试中不能使用带有箭头功能的this.*,因为this.*会引用箭头功能的词汇语境,而不是摩卡的语境。对于this关键字的任何使用,您将需要切换您的 Cypress 测试以使用常规的function () {}语法,而不是() => {}

别名在共享上下文中的另一个重要用途是与 Cypress 设备一起使用。夹具是 Cypress 用来提供测试中使用的模拟数据的功能。夹具是在文件中创建的,可以在测试中访问。

重要说明

夹具提供测试数据,我们利用夹具来提供与应用期望的输入或执行操作时生成的输出一致的数据。对于我们来说,夹具是一种为测试提供数据输入的简单方法,而无需在测试中硬编码数据或在测试运行时自动生成数据。有了夹具,我们还可以为不同的测试利用相同的测试数据集合。

假设有一个包含所有创建的待办事项列表的todos fixture,我们可以进行一个类似于下面代码块的测试:

describe('Todo fixtures', () => {
    beforeEach(() => {
      // alias the todos fixtures
      cy.get(".new-todo").type("New Todo {Enter}");
      cy.get('.todo-list>li:nth-
        child(1)').invoke('text').as('todoItem')
      cy.fixture('todos.json').as('todos')
    })

    it('todo fixtures have name', function () {
      // access the todos property
      const todos = this.todos[0]

      // make sure the first todo item contains the first
      // todo item name
      expect(this.todoItem).to.contain(todos.name)
    })
  })

在前面的代码块中,我们混淆了创建的待办事项和包含创建的待办事项的todos.json夹具文件。我们可以在所有测试中使用待办事项的夹具,因为我们将夹具装入了测试的beforeEach挂钩中。在这个测试中,我们使用this.todo[0]访问了我们的第一个夹具值,这是我们的待办事项数组中的第一个对象。为了进一步了解如何使用夹具和我们正在使用的确切文件,请看一下我们在本章开始时在cypress/fixtures directory下克隆的 GitHub 存储库。

重要说明

Cypress 仍然使用异步命令工作,试图访问beforeEach钩子外的this.todos将导致测试失败,因为测试首先需要加载夹具,然后才能使用它们。

在共享上下文的同时,Cypress 命令还可以使用一个特殊的'@'命令,这样在引用已声明别名的上下文时就不用使用this.*了。以下代码块显示了'@'语法在引用 Cypress 别名时的用法:

it('todo fixtures have name', () => {
      // access the todos property
      cy.get('@todos').then((todos) => {
        const todo = todos[0]
      // make sure the first todo item contains the first
      // todo item name
      expect(this.todoItem).to.contain(todo.name)
      });
    });

在前面的代码块中,我们使用了cy.get()命令来消除访问夹具文件时的this.*语法,以及使用旧式函数声明方法的需要。当我们使用this.todos时,我们是同步访问todos对象,而当我们引入cy.get('@todos')时,我们是异步访问todos对象。

如前所述,当 Cypress 同步运行代码时,命令按照调用的顺序执行。另一方面,当我们异步运行 Cypress 测试时,来自已执行命令的响应不会按照调用命令的顺序返回,因为命令的执行不会按照调用命令的顺序进行。在我们的例子中,this.todo将作为同步命令执行,这将按照执行的顺序返回todo对象结果,而cy.get('@todos')将表现得像异步命令,并且当它们可用时将返回todo对象响应。

访问元素引用

别名也可以用来访问 DOM 元素进行重用。引用元素可以确保一旦 DOM 元素被别名引用,我们就不需要重新声明它们了。在下面的代码块中,我们将为输入元素创建一个别名来添加新的待办事项,并在以后创建待办事项时引用它:

it('can add a todo - DOM element access reference', () => {
      cy.get(".new-todo").as('todoInput');
      // Aliased todo input element
      cy.get('@todoInput').type("New Todo {Enter}");
      cy.get('@todoInput').type("Another New Todo {Enter}");
      cy.get(".todo-list").find('li').should('have.length', 2)
  });

这个测试展示了使用别名来访问 DOM 中作为引用存储的元素。在测试中,Cypress 查找我们保存的'todoInput'引用,使用它而不是运行另一个查询来查找我们的输入项。

访问路线参考

我们可以使用别名来为测试中的应用引用路由。路由管理网络请求的行为,通过使用别名,我们可以确保发出正确的请求,发送服务器请求,并在发出请求时创建正确的 XHR 对象断言。以下代码块显示了使用路由时别名的用法:

it('can wait for a todo response', () => {
      cy.server()
      cy.intercept('POST', '/todos', { id: 123 }).as('todoItem')
      cy.get('form').submit()
      cy.wait('@todoItem').its('requestBody')
        .should('have.property', 'name', 'New Todo')
      cy.contains('Successfully created item: New Todo')
    });

在这个代码块中,我们引用了我们的todoItem请求作为别名。然后,路由请求将检查我们提交的表单是否已成功提交,并返回响应。当在路由中使用别名时,我们不必一直引用或调用路由,因为 Cypress 已经存储了我们之前创建的别名对路由的响应。

访问请求引用

就像使用别名访问路由引用时,我们可以使用 Cypress 访问 Cypress 请求,稍后使用请求的属性。在下面的代码块中,我们识别特定注释的请求,并使用别名检查注释的属性:

it('can wait for a comment response', () => {
      cy.request('https://jsonplaceholder.cypress.io/comments/6')
    .as('sixthComment');
      cy.get('@sixthComment').should((response) => {
        expect(response.body.id).to.eq(6)
    });
 });

测试对特定的注释进行断言,并检查断言是否与注释的标识匹配。我们使用了一个别名来引用请求网址,这样在运行我们的测试时,我们只需要引用我们已经别名的网址,而不需要完整地键入它。以下运行测试的屏幕截图显示了 Cypress 如何创建一个别名,供其在运行测试时参考:

Figure 8.5 – Request references

图 8.5–请求参考

在前面的截图中,第一个sixthComment命令是 Cypress 创建别名的命令,第二个是当运行的测试识别了别名,并根据从别名网址获得的响应断言期望。

重述–了解 Cypress 别名

在这一节中,我们学习了关于别名以及如何使用别名为我们的测试编写“干净”的代码,方法是为我们提供一种方法来访问和引用我们稍后在测试中可能需要的请求、元素、路由和命令。我们还学习了如何访问 Cypress 别名:要么通过在别名前使用@符号的异步方法,要么通过使用this关键字直接访问别名对象的同步方法。最后,我们学习了如何在测试中使用别名来引用元素,使我们能够在测试中使用别名路由和请求。

总结

在本章中,我们学习了别名和变量,以及如何在 Cypress 中使用它们。我们介绍了 Cypress 测试中的变量,不同类型的变量及其范围,以及如何利用它们。我们还介绍了 Cypress 中的变量如何帮助创建闭包,以及我们如何创建一个除了测试可访问的全局上下文之外,只能由变量访问的环境。最后,我们研究了如何使用别名以及使用别名的不同环境。我们学习了如何在测试中引用别名,如何将它们用于元素、路由和请求,甚至用于测试钩子和测试本身之间的上下文共享。

通过本章,您已经掌握了理解别名和变量如何工作、别名如何在异步和同步场景中使用以及如何以及何时在测试中创建和实现变量范围的技巧。

现在,您已经完全理解了别名和变量是如何工作的,我们已经为下一章做好了准备,在这一章中,我们将了解测试运行器是如何工作的。我们将深入到测试者的不同方面,以及如何解释测试者身上发生的事件。