Mirage JS 深入探讨:使用 Mirage JS 和 Cypress 进行 UI 测试(第 4 部分)
已发表: 2022-03-10我最喜欢的关于软件测试的引用之一来自 Flutter 文档。 它说:
“当您添加更多功能或更改现有功能时,如何确保您的应用程序继续运行? 通过编写测试。”
关于这一点,Mirage JS Deep Dive 系列的最后一部分将重点介绍使用 Mirage 测试您的 JavaScript 前端应用程序。
注意:本文假设 Cypress 环境。 Cypress 是一个用于 UI 测试的测试框架。 但是,您可以将这里的知识转移到您使用的任何 UI 测试环境或框架中。
阅读该系列的前几部分:
- 第 1 部分:了解 Mirage JS 模型和关联
- 第 2 部分:了解工厂、夹具和序列化程序
- 第 3 部分:了解时序、响应和直通
UI 测试入门
UI 或用户界面测试是一种验收测试形式,用于验证前端应用程序的用户流程。 这类软件测试的重点是最终用户,也就是将在各种设备(从台式机、笔记本电脑到移动设备)上与您的 Web 应用程序交互的实际人。 这些用户将使用键盘、鼠标或触摸屏等输入设备与您的应用程序交互或交互。 因此,编写 UI 测试是为了尽可能地模拟用户与您的应用程序的交互。
我们以电子商务网站为例。 一个典型的 UI 测试场景是:
- 用户在访问主页时可以查看产品列表。
其他 UI 测试场景可能是:
- 用户可以在产品的详细信息页面上看到产品的名称。
- 用户可以点击“加入购物车”按钮。
- 用户可以结账。
你明白了,对吧?
在进行 UI 测试时,您将主要依赖后端状态,即它是返回产品还是错误? Mirage 在其中扮演的角色是让您可以根据需要调整这些服务器状态。 因此,您无需在 UI 测试中向生产服务器发出实际请求,而是向 Mirage 模拟服务器发出请求。
对于本文的剩余部分,我们将在一个虚构的电子商务 Web 应用程序 UI 上执行 UI 测试。 所以让我们开始吧。
我们的第一个 UI 测试
如前所述,本文假设 Cypress 环境。 赛普拉斯使 Web 上的 UI 测试变得快速而简单。 您可以模拟点击和导航,并且可以以编程方式访问应用程序中的路线。 有关赛普拉斯的更多信息,请参阅文档。
因此,假设我们可以使用 Cypress 和 Mirage,让我们首先为您的 API 请求定义一个代理函数。 我们可以在 Cypress 设置的support/index.js
文件中这样做。 只需将以下代码粘贴到:
// cypress/support/index.js Cypress.on("window:before:load", (win) => { win.handleFromCypress = function (request) { return fetch(request.url, { method: request.method, headers: request.requestHeaders, body: request.requestBody, }).then((res) => { let content = res.headers.map["content-type"] === "application/json" ? res.json() : res.text() return new Promise((resolve) => { content.then((body) => resolve([res.status, res.headers, body])) }) }) } })
然后,在您的应用引导文件(Vue 为main.js
,React 为index.js
)中,我们将仅在赛普拉斯运行时使用 Mirage 将应用的 API 请求代理到handleFromCypress
函数。 这是代码:
import { Server, Response } from "miragejs" if (window.Cypress) { new Server({ environment: "test", routes() { let methods = ["get", "put", "patch", "post", "delete"] methods.forEach((method) => { this[method]("/*", async (schema, request) => { let [status, headers, body] = await window.handleFromCypress(request) return new Response(status, headers, body) }) }) }, }) }
通过该设置,无论何时赛普拉斯运行,您的应用都知道使用 Mirage 作为所有 API 请求的模拟服务器。
让我们继续编写一些 UI 测试。 我们将首先测试我们的主页,看看它是否显示了5 个产品。 要在 Cypress 中执行此操作,我们需要在项目目录根目录的tests
文件夹中创建一个homepage.test.js
文件。 接下来,我们将告诉赛普拉斯执行以下操作:
- 访问主页ie
/
route - 然后断言它是否具有
product
类的 li 元素,并检查它们的数量是否为 5。
这是代码:
// homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
您可能已经猜到此测试会失败,因为我们没有生产服务器将 5 个产品返回到我们的前端应用程序。 那么我们该怎么办? 我们在 Mirage 中模拟了服务器! 如果我们引入 Mirage,它可以拦截我们测试中的所有网络调用。 让我们在下面执行此操作,并在 beforeEach 函数中的每个测试之前启动 Mirage 服务器,并在beforeEach
函数afterEach
其关闭。 beforeEach
和afterEach
函数都由 Cypress 提供,它们是可用的,因此您可以在测试套件中的每个测试运行之前和之后运行代码——因此得名。 所以让我们看一下代码:
// homepage.test.js import { Server } from "miragejs" let server beforeEach(() => { server = new Server() }) afterEach(() => { server.shutdown() }) it("shows the products", function () { cy.visit("/") cy.get("li.product").should("have.length", 5) })
好的,我们正在取得进展; 我们已经从 Mirage 导入了服务器,我们分别在beforeEach
和afterEach
函数中启动和关闭它。 让我们开始模拟我们的产品资源。
// homepage.test.js import { Server, Model } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
注意:如果您不了解上述代码片段的 Mirage 位,可以随时查看本系列的前几部分。
- 第 1 部分:了解 Mirage JS 模型和关联
- 第 2 部分:了解工厂、夹具和序列化程序
- 第 3 部分:了解时序、响应和直通
好的,我们已经开始通过创建产品模型以及为/api/products
路由创建路由处理程序来充实我们的服务器实例。 但是,如果我们运行测试,它将失败,因为我们在 Mirage 数据库中还没有任何产品。
让我们用一些产品填充 Mirage 数据库。 为了做到这一点,我们可以在我们的服务器实例上使用create()
方法,但是手动创建 5 个产品似乎相当乏味。 应该有更好的方法。
啊,是的,有。 让我们利用工厂(如本系列第二部分所述)。 我们需要像这样创建我们的产品工厂:
// homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
然后,最后,我们将使用createList()
快速创建我们的测试需要通过的 5 个产品。
我们开工吧:
// homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList("product", 5) cy.visit('/'); cy.get('li.product').should('have.length', 5); });
因此,当我们运行测试时,它通过了!
注意:每次测试后,Mirage 的服务器都会关闭并重置,因此此状态不会在测试中泄漏。
避免使用多个 Mirage 服务器
如果您一直关注本系列,您会注意到我们在开发中使用 Mirage 拦截我们的网络请求; 我们在应用程序的根目录中有一个server.js
文件,我们在其中设置了 Mirage。 本着 DRY(不要重复自己)的精神,我认为利用该服务器实例而不是为开发和测试使用两个单独的 Mirage 实例会更好。 为此(如果您还没有server.js
文件),只需在您的项目src目录中创建一个。

注意:如果您使用的是 JavaScript 框架,您的结构会有所不同,但总体思路是在项目的 src 根目录中设置 server.js 文件。
因此,使用这个新结构,我们将在server.js
中导出一个函数,该函数负责创建我们的 Mirage 服务器实例。 让我们这样做:
// src/server.js export function makeServer() { /* Mirage code goes here */}
让我们通过移除我们在homepage.test.js
中创建的 Mirage JS 服务器并将其添加到makeServer
函数体中来完成makeServer
函数的实现:
import { Server, Model, Factory } from 'miragejs'; export function makeServer() { let server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }
现在您所要做的就是在您的测试中导入makeServer
。 使用单个 Mirage Server 实例更简洁; 这样您就不必为开发和测试环境维护两个服务器实例。
导入makeServer
函数后,我们的测试现在应该如下所示:
import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer(); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });
因此,我们现在拥有了一个中央 Mirage 服务器,为我们提供开发和测试服务。 您还可以使用makeServer
函数在开发中启动 Mirage(请参阅本系列的第一部分)。
您的 Mirage 代码不应进入生产环境。 因此,根据您的构建设置,您只需在开发模式下启动 Mirage。
注意:阅读我关于如何使用 Mirage 和 Vue.js 设置 API 模拟的文章,了解我是如何在 Vue 中做到这一点的,这样你就可以在你使用的任何前端框架中进行复制。
测试环境
Mirage 有两个环境:开发(默认)和测试。 在开发模式下,Mirage 服务器的默认响应时间为 400 毫秒(您可以自定义。请参阅本系列的第三篇文章),将所有服务器响应记录到控制台,并加载开发种子。
但是,在测试环境中,我们有:
- 0 延迟以保持我们的测试快速
- Mirage 会抑制所有日志,以免污染您的 CI 日志
- Mirage 还将忽略 seed
seeds()
函数,以便您的种子数据可以仅用于开发,但不会泄漏到您的测试中。 这有助于保持测试的确定性。
让我们更新我们的makeServer
,以便我们可以从测试环境中受益。 为此,我们将使它接受一个带有 environment 选项的对象(我们将默认它为 development 并在我们的测试中覆盖它)。 我们的server.js
现在应该如下所示:
// src/server.js import { Server, Model, Factory } from 'miragejs'; export function makeServer({ environment = 'development' } = {}) { let server = new Server({ environment, models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }
另请注意,我们使用 ES6 属性简写将环境选项传递给 Mirage 服务器实例。 现在有了这个,让我们更新我们的测试以覆盖要测试的环境值。 我们的测试现在看起来像这样:
import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });
AAA 测试
Mirage 鼓励采用称为 AAA 或 AAA 测试方法的测试标准。 这代表Arrange 、 Act和Assert 。 你已经可以在我们上面的测试中看到这个结构了:
it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })
您可能需要打破这种模式,但 10 次中有 9 次它应该对您的测试正常工作。
让我们测试错误
到目前为止,我们已经测试了我们的主页是否有 5 个产品,但是,如果服务器关闭或获取产品出现问题怎么办? 在这种情况下,我们不需要等待服务器关闭来处理我们的 UI 的外观。 我们可以简单地用 Mirage 模拟这种情况。
当用户在主页上时,让我们返回 500(服务器错误)。 正如我们在上一篇文章中看到的,为了自定义 Mirage 响应,我们使用了 Response 类。 让我们导入它并编写我们的测试。
homepage.test.js import { Response } from "miragejs" it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });
多么灵活的世界! 我们只是覆盖 Mirage 将返回的响应,以便测试我们的 UI 在获取产品失败时将如何显示。 我们的整个homepage.test.js
文件现在看起来像这样:
// homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });
请注意,我们对/api/products
处理程序所做的修改只存在于我们的测试中。 这意味着它在您处于开发模式时按照我们之前定义的方式工作。
因此,当我们运行测试时,两者都应该通过。
注意:我认为值得一提的是,我们在 Cypress 中查询的元素应该存在于您的前端 UI 中。 赛普拉斯不会为您创建 HTML 元素。
测试产品详细信息页面
最后,我们来测试一下商品详情页的UI。 所以这就是我们正在测试的:
- 用户可以在产品详情页面看到产品名称
让我们开始吧。 首先,我们创建一个新的测试来测试这个用户流。
这是测试:
it("shows the product's name on the detail route", function() { let product = this.server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });
你的homepage.test.js
最终应该是这样的。
// homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { console.log(server); server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); }); it("shows the product's name on the detail route", function() { let product = server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });
当您运行测试时,这三个都应该通过。
包起来
在本系列中向您展示 Mirage JS 的内部非常有趣。 通过使用 Mirage 模拟后端服务器,我希望您能够更好地开始拥有更好的前端开发体验。 我也希望您能利用本文中的知识为您的前端应用程序编写更多的验收/UI/端到端测试。
- 第 1 部分:了解 Mirage JS 模型和关联
- 第 2 部分:了解工厂、夹具和序列化程序
- 第 3 部分:了解时序、响应和直通
- 第 4 部分:使用 Mirage JS 和 Cypress 进行 UI 测试