十一、练习——拦截和嗅探 XHR 请求

在我们开始这一章之前,您需要了解为什么我们需要存根或间谍请求和方法,为此,您需要了解 Cypress 请求以及如何测试单个方法。前面几章已经介绍了关于如何轻松开始使用 Cypress 的大量知识,我们还介绍了与网络请求和功能测试相关的概念。在这一章中,我们将建立在前几章中获得的概念,重点是通过使用示例和练习的实际操作方法。

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

  • 理解 XHR 的要求
  • 了解如何拦截请求
  • 了解如何在测试中发现方法

一旦你完成了这些主题中的每一个,你就可以开始使用 Cypress 进行视觉测试了。

技术要求

首先,我们建议您克隆包含源代码以及我们将在本章中从 GitHub 编写的所有测试、练习和解决方案的存储库。

本章的 GitHub 存储库可以在的网站找到。本章的源代码可以在chapter-11目录中找到。

在我们的 GitHub 存储库中,我们有一个财务测试应用,将用于本章的不同示例和练习。

重要提示:在 Windows 中运行命令

注意:默认的 Windows 命令提示符和 PowerShell 不能正确解析目录位置。

请遵循进一步列出的专门在以单词*windows为后缀的 Windows 操作系统上工作的 Windows 命令。

要确保测试应用正在您的计算机上运行,请从您计算机上终端中应用的根目录运行以下命令:

$ cd cypress/chapter-11;
$ npm install -g yarn or sudo npm install -g yarn
$ npm run cypress-init; (for Linux or Mac OS)
$ npm run cypress-init-windows; (for Windows OS)
// run this command if it's the first time running the application
or
$ npm run cypress-app (for Linux or Mac OS)
$ npm run cypress-app-windows; (for Windows OS)
// run this command if you had already run the application previously
Optionally
$ npm run cypress-app-reset; (for Linux or Mac OS)
$ npm run cypress-app-reset-windows; (for Windows OS)
// run this command to reset the application state after running your tests

重要说明

我们的测试位于chapter-11目录中,测试应用位于存储库的根目录中。为了正确运行我们的测试,我们必须同时运行我们的应用和 Cypress 测试,因为测试是在运行于我们机器本地的实时应用上运行的。还需要注意的是,应用将需要使用端口3000作为前端应用,使用端口3001作为服务器应用。

第一个命令将导航到我们的应用所在的cypress-realworld-app目录。然后npm run cypress-init命令将安装应用运行所需的依赖项,npm run cypress-app命令将启动应用。或者,您可以使用npm run cypress-app-reset命令重置应用状态。重置应用会删除已添加的不属于应用的任何数据,从而将应用状态恢复到克隆存储库时的状态。

理解 XHR 的要求

XMLHttpRequest(XHR)是一个存在于所有现代浏览器中的应用编程接口,采用对象的形式,其方法用于在发送请求的网络浏览器和提供响应的网络服务器之间传输数据。XHR 应用编程接口是独一无二的,因为我们可以使用它来更新浏览器页面,而无需重新加载页面,在页面加载后请求和接收服务器数据,甚至将数据作为后台任务发送到服务器。在本节中,我们将介绍 XHR 请求的基础知识,以及在编写我们的 Cypress 测试的过程中它们的重要性。

在测试中利用 XHR 请求

XHR 请求是开发人员的梦想,因为它们允许您静默地发送和接收来自服务器的数据,而无需担心客户端应用需要重新加载才能执行操作时的错误或等待时间等问题。虽然 XHR 对开发人员来说是一个梦想,但对测试人员来说却是一场噩梦,因为它引入了不确定性,例如无法知道请求何时会完成处理,甚至无法知道数据何时从服务器返回。

为了解决 XHR 的不确定性问题,Cypress 引入了cy.intercept()命令,我们在网络请求部分的 第 9 章Cypress 测试跑者的高级使用,第 10 章练习-导航和网络请求,中对此进行了深入研究。cy.intercept()命令监听 XHR 的响应,并知道 Cypress 何时返回了对特定 XHR 请求的响应。使用cy.intercept()命令,我们可以指示 Cypress 等待,直到收到特定请求的响应,这使得我们在编写等待服务器响应的测试时更加确定。

xhr-requests/xhr.spec.js文件中的以下代码块显示了将用户登录到我们的财务测试应用的代码。当用户登录时,应用向服务器发送请求,以加载应用所需的通知、银行帐户和交易详细信息。这些详细信息作为 XHR 响应从应用编程接口服务器返回:

describe('XHR Requests', () => {
    it('logs in a user', () => {
        cy.intercept('bankAccounts').as('bankAccounts');
        cy.intercept('transactions/public').
        as('transactions');
        cy.intercept('notifications').as('notifications');
        cy.visit('signin'); 
        cy.get('#username').type('Katharina_Bernier');
        cy.get('#password').type('s3cret');
        cy.get('[data-test="signin-submit"]').click()
        cy.wait('@bankAccounts').its('response.statusCode'
        ).should('eq', 200);
        cy.wait('@transactions').its('response.statusCode
        ').should('eq', 304);
        cy.wait('@notifications').its('response.statusCode
        ').should('eq', 304);
    });
});

在前面的代码块中,我们让一个用户登录,等待 Cypress 返回 XHR 对我们从服务器发送的交易、通知和银行账户请求的响应。响应仅在用户登录尝试成功时发送。在下面的截图中,我们可以看到 Cypress 如何在测试中处理 XHR 请求:

Figure 11.1 – XHR requests and responses from the server

图 11.1–来自服务器的 XHR 请求和响应

此截图显示了向我们的服务器发出/bankAccounts/transactions/notificationsXHR 请求的应用。为了使我们的测试具有确定性并等待指定的时间以确保成功登录,我们使用cy.intercept()命令来检查来自 XHR 请求的响应何时被服务器发回,以及它们是否以正确的状态代码发回。

与没有任何处理等待的失败机制或具有显式时间等待的测试相比,在测试中等待 XHR 响应给了我们显著的优势。等待 XHR 回应的替代方法是明确等待特定的时间,这只是一个估计,而不是 Cypress 等待特定回应的确切时间。运行测试时等待路由响应的一些优势如下:

  • 能够断言从路由返回的 XHR 响应对象
  • 创建健壮的测试,从而减少剥落
  • 由于其准确性,故障信息可以被理解
  • 能够存根响应和“假”服务器响应

突出这些优势后,使用 XHR 请求有助于我们确定何时收到响应,以及在收到应用所需的所有响应后,Cypress 何时可以继续执行我们的命令。

回顾–在测试中利用 XHR 请求

在本节中,我们了解了 XHR 请求,它们是什么,以及 Cypress 如何利用它们从应用服务器发送和获取请求。我们还学习了如何通过确定性地等待服务器响应来等待 XHR 响应,从而减少不可靠的测试。我们还了解了 XHR 如何帮助我们,如何获得准确的故障消息,甚至如何从服务器响应中断言响应。最后,我们介绍了如何将cy.intercept()命令用于 XHR 响应,以及通过减少测试不确定性来控制测试执行的潜在好处。在下一节中,我们将研究使用存根来控制来自服务器的 XHR 响应。

了解如何拦截请求

现在我们知道了什么是 XHR 请求,知道如何帮助 Cypress 测试 XHR 请求是很重要的,更好的是,我们如何避免来自服务器的实际响应,而是创建我们自己的“假”响应,我们的应用会将其解释为从服务器发送的实际响应。在本节中,我们将研究如何将 XHR 请求存根到服务器,何时存根请求,以及存根服务器请求在我们的测试中的影响。

掐灭 XHR 的请求

Cypress 允许用户灵活地让他们的请求到达服务器,或者当应用向服务器端点发出请求时,让他们做出响应。借助 Cypress 的灵活性,我们甚至能够允许一些请求传递到服务器,同时拒绝其他请求并将其存根化。剔除 XHR 的回答为我们的测试增加了一层控制。通过存根,我们可以控制返回给客户端的数据,并且我们可以更改正文状态标题的响应,如果我们想在服务器响应中模拟网络延迟,甚至可以引入延迟。

存根请求的优点

拦截请求让我们能够更好地控制返回给测试的响应,以及向服务器发出请求的客户端将接收到的数据。以下是阻止我们的请求的优点:

  • 控制响应的正文、标题和状态。
  • 响应的快速响应时间。
  • 服务器不需要任何代码更改。
  • 网络延迟模拟可以添加到请求中。

接下来,我们也来看看一些缺点。

存根请求的缺点

虽然存根是处理测试中的 XHR 对 Cypress 客户端应用的响应的一种很好的方式,但它也给我们的测试过程带来了一些负面影响,如下所述:

  • 无法在某些服务器端点上进行测试覆盖
  • 不能保证响应数据和存根数据匹配

建议在大多数测试中存根 XHR 响应,以减少执行测试所花费的时间,并从服务器获得存根和实际 API 响应的健康组合。在使用 JSON APIs 时,Cypress 中的 XHR 存根也是最适合的。

在下面的代码块中,在xhr-requests/xhr-stubbing.spec.js文件中,我们将存根化bankAccounts端点,并避免在运行应用时向服务器发出实际请求:

describe('XHR Stubbed Requests', () => {    
    it('Stubs bank Account XHR server response', () => {
        cy.intercept('GET', 'bankAccounts',
        {results: [{id :"RskoB7r4Bic", userId :"t45AiwidW",
        bankName: "Test Bank Account", accountNumber 
        :"6123387981", routingNumber :"851823229", 
        isDeleted: false}]})
        .as('bankAccounts');
        cy.intercept('GET', 'transactions/public').
        as('transactions');
        cy.intercept('notifications').as('notifications');
        cy.wait('@transactions').its('
        response.statusCode').should('eq', 304);
        cy.wait('@notifications').its('
        response.statusCode').should('eq', 304);
        cy.wait('@bankAccounts').then((xhr) => {
            expect(xhr.response.body.results[0].
            bankName).to.eq('Test Bank Account')
            expect(xhr.response.body.results[0].
            accountNumber).to.eq('6123387981')  
        });
    });
});

在前面的代码块中,我们已经存根化了/bankaccounts服务器响应,我们没有等待响应,而是自己提供了一个响应,这个响应几乎与服务器会发回的响应相同。下面的截图显示了成功的存根响应和我们使用存根响应向客户提供的“假”存根银行账户信息:

Figure 11.2 – Stubbed XHR response in the client application

图 11.2–客户端应用中的存根 XHR 响应

图 11.2 中,我们可以看到,几乎无法判断我们的响应是被存根还是从我们的服务器收到的。使用丝柏,客户端应用无法识别响应是真正从服务器发送还是被拦截,这一特性使丝柏成为拦截请求和发送响应的有效工具,否则将花费很长时间从服务器发送响应。在下面的练习中,我们将了解更多关于消除 XHR 响应的信息。

练习 1

使用位于cypress-realworld-app目录下的 GitHub 存储库中提供的金融应用,执行以下练习来测试您在拦截 XHR 响应方面的知识。该练习的解决方案可以在chapter-11/integration/xhr-requests-exercises目录中找到:

  1. Stub the login POST request of the application and instead of returning the name of the test user in the dashboard, change it to reflect your name and your username.

    断言返回的响应确实包含了您的用户名和姓名信息。以下屏幕截图显示了页面上应该更改的信息:

    Figure 11.3 – Change the name and username by stubbing the login response

    图 11.3–通过存根化登录响应来更改名称和用户名

    重要说明

    为了正确地拦截响应,您需要了解当路由没有被拦截时,服务器发送什么作为响应。为此,在浏览器上打开浏览器控制台,单击网络选项卡,然后选择 XHR 过滤器选项。现在,您可以看到发送到服务器并由客户端接收的所有响应和请求。要将特定请求转换为存根,您应该单击确切的请求,并从浏览器控制台的网络窗口的响应选项卡中复制响应。确切的响应(或结构相似的响应)是我们应该用来存根我们的服务器响应,以确保对我们的客户端的响应的一致性。从网络窗口,我们还可以获得信息,如与请求一起发送和接收的标题,以及用于向服务器发送请求的实际网址。

    以下截图显示了来自服务器的通知 XHR 响应的示例:

    Figure 11.4 – Server XHR response for notifications endpoint on the Chrome browser console

    图 11.4–Chrome 浏览器控制台上通知端点的服务器 XHR 响应

  2. 成功登录后,从Everyone Dashboard选项卡中选择一笔随机交易,并将交易金额修改为 100 美元。

通过本练习,您不仅学习了如何截取 XHR 响应,还学习了客户端如何处理从服务器接收的数据。通过了解 XHR 响应存根的好处,您现在已经准备好处理涉及存根响应的复杂 Cypress 测试。

回顾–了解如何拦截请求

在本节中,我们学习了如何使用 XHR 服务器请求来接收请求,以及如何使用存根拦截发送的请求。我们还学习了如何使用存根来控制我们发送回客户端应用的响应的性质,以及如何断言我们的存根响应,这些响应看起来类似于我们从服务器接收到的客户端响应。最后,我们学习了如何使用浏览器来识别哪些响应是存根,并使用我们正在存根的响应的内容。在下一节中,我们将看看间谍是如何工作的,以及我们如何在我们的 Cypress 方法中利用它。

了解如何在测试中窥探方法

间谍与存根密切相关的区别在于,与存根不同的是,存根可用于修改一个方法或请求的数据,间谍只获取该方法或请求的状态,不具备修改该方法或请求的能力。他们就像现实生活中的间谍一样工作,只跟踪和报告。间谍帮助我们理解测试的执行,什么元素被调用,什么被执行。在这一节中,我们将学习在 Cypress 进行间谍活动的概念,间谍活动方法的优势,以及我们如何利用间谍活动编写更好的 Cypress 测试。

为什么是间谍?

我们在 Cypress 使用间谍记录方法中的调用以及方法的参数。通过使用间谍,我们可以断言一个特定的方法被调用了一定的次数,并且是用正确的参数调用的。我们甚至可以知道方法的返回值是什么,或者方法在被调用时的执行上下文。Spies 主要用于单元测试环境中,但也有集成环境中的应用,例如测试两个功能是否相互适当集成,以及它们在一起执行时是否协调工作。执行时,cy.spy()命令返回一个值,而不是像几乎所有其他 Cypress 命令一样返回一个承诺。cy.spy()命令没有超时,不能与其他 Cypress 命令进一步链接。

间谍的优势

以下是在测试中使用间谍的一些优势:

  • 间谍不会修改被调用的请求或方法。
  • 间谍让你能够快速验证是否调用了方法。
  • 它们提供了一种测试功能集成的简单方法。

间谍活动是一个有趣的概念,因为它引入了一种监控方法的方式,而不必对结果采取行动。在下面的代码块中,我们有一个测试,它包含一个简单的函数来对两个数字求和:

  it('cy.spy(): calls sum method with arguments', () => {
        const obj = {
            sum(a, b) {
                return a + b
            }
        }
        const spyRequest = cy.spy(obj, 'sum').as('sumSpy');
        const spyRequestWithArgs = spyRequest.withArgs(1, 
        2).as('sumSpyWithArgs')
        obj.sum(1, 2); //spy trigger 
        expect(spyRequest).to.be.called;
        expect(spyRequestWithArgs).to.be.called;
        expect(spyRequest.returnValues[0]).to.eq(3);
    });

在前面的方法中,我们已经设置了cy.spy()来监控我们的sum方法,并在调用该方法时或者在用参数调用该方法时触发间谍。每当调用该方法时,我们的间谍都会记录它被调用的次数,我们也可以继续检查我们的方法是否被任何参数调用。sum函数在一个 JavaScript 对象中,间谍方法的触发器是obj.sum(1, 2) sum 函数调用,它是在测试中执行我们的断言之前调用的。下面的截图显示了测试的间谍、呼叫数量和别名:

Figure 11.5 – Spying on a sum method

图 11.5–监视求和方法

看看这个在sum()函数上使用cy.spy()方法的方法,我们可以看到sum方法和用参数调用的sum方法的间谍都是在sum方法开始执行时触发的。

在下一个示例中,我们将探索一个更复杂的场景,在这个场景中,我们将尝试监视一个从服务器返回我们的 JSON 数据库中所有事务的方法。下面的代码块显示了将获取我们所有事务的方法的间谍:

it('cy.spy(): fetches all transactions from our JSON database', () => {
        const obj = {
          fetch(url, method) {
                return cy.request({
                    url,
                    method
                }).then((response) => response);
            }
        }
        const spyRequest = cy.spy(obj, 
        'fetch').as('reqSpy');
        obj.fetch('http://localhost:3001/transactions', 
        'GET');
        expect(spyRequest).to.be.called;
        expect(spyRequest.args[0][0]).to.eq
        ('http://localhost:3001/transactions')
        expect(spyRequest.args[0][1]).to.eq('GET');
    });

在这个测试中,我们正在验证从数据库获取事务的请求是否发生。通过这个测试,我们可以监控我们的方法,并检查当我们的方法被调用时,正确的参数是否被传递给了它。

很明显,有了间谍,我们能够识别调用了哪些方法,调用了多少次,调用方法时使用了哪些参数。我们将在下面的练习中了解更多关于间谍的知识。

练习 2

使用 GitHub 存储库中提供的位于cypress-realworld-app目录中的金融应用,进行以下练习,以测试您剔除 XHR 回复的知识。该练习的解决方案可以在chapter-11/integration/spies-exercise目录中找到:

  1. 创建一个名为Area的方法来计算三角形的面积,监视area方法,并调用该方法来断言area方法确实被cy.spy()调用和监视。断言该方法也用baseheight参数调用。
  2. 使用我们的应用,以用户身份登录,并监视 API 请求方法以获取该登录用户的所有银行帐户。断言调用了向服务器发出 API 请求的方法,并且参数作为参数传递给了该方法。

本练习将帮助您了解间谍如何在 Cypress 上工作,以及可以与cy.spy()一起使用的不同方法,以找到被监视的方法的内容。通过监视方法,我们也能够知道方法参数是否被调用,如何被调用,以及返回值。

回顾–了解如何在测试中发现方法

在本节中,我们学习了间谍活动,涵盖了间谍活动有多重要,以及它与存根有多不同,因为我们不被允许更改被监视的方法或请求的值。我们还学习了如何使用存根识别方法的参数、方法被调用的次数、执行上下文以及被监视的方法的返回值。使用示例和练习,我们还可以与cy.spy()命令进行交互,这有助于我们理解该命令以及它如何在 f 方法的上下文中工作。

总结

本章的重点主要是 XHR 请求和响应,以及它们如何与客户端和服务器交互。我们首先了解什么是 XHR 请求和响应,以及当我们想从客户端发送请求和从服务器接收请求时,它们有多重要。在本章中,我们还研究了如何使用cy.intercept()命令中内置的 Cypress 存根功能来存根化 XHR 响应,从而“伪造”服务器响应。最后,我们探索了 Cypress cy.spy()命令,它进一步让我们了解了如何监控 Cypress 中的方法,并获得了查找方法执行次数、执行方式、参数甚至返回值的能力。在最后一节中,我们学习了了解的重要性,即通过间谍活动,我们只能“观察”执行是如何发生的,而不一定有能力改变请求的执行过程或正在测试的方法。

我相信通过这一章,你已经掌握了知道 XHR 请求是什么,它们是如何工作的,如何存根,以及如何窥探 Cypress 方法的技巧。在下一章中,我们将看看在 Cypress 进行的可视化测试。