应用程序测试
Mirage 最棒的一点在于它使 JavaScript 应用程序开发人员能够编写高级 UI 测试,而无需与他们的生产 API 服务器交互。
这些测试旨在验证应用程序的用户流程。在这种情况下,用户是指将在计算机或移动设备上使用您的网络应用程序,并使用键盘、鼠标或其他输入设备进行操作的实际人员。因此,这些测试应该与这些人如何在现实世界中与您的应用程序交互的方式非常相似。
让我们考虑一个您想要测试的用户流程示例
用户访问主页时可以查看电影列表
大多数像这样的应用程序测试实际上依赖于给定的服务器状态,即使它们省略了它或让它隐式存在。而这正是 Mirage 的用武之地:它帮助您将服务器状态明确地纳入测试。
如果我们要将上述测试改写得更完整,我们可能会得到类似这样的内容
- 假设服务器上存在 10 个电影资源
- 当用户访问主页时
- 那么他们应该看到一个包含 10 部电影的列表
让我们看看如何使用 Mirage 编写此测试。
我们的第一个测试
此示例使用 Cypress 进行语法演示,但 Mirage 可以与您设置的任何 JavaScript 测试框架配合使用。
请参阅我们的 Cypress 快速入门,了解如何在使用 Cypress 的应用程序中使用 Mirage。
假设 Cypress 已连接,我们可以编写以下测试
// homepage-test.js
it("shows the movies", () => {
cy.visit("/")
cy.get("li.selected").should("have.length", 10)
})
我们的应用程序运行,但当它向 /api/movies
发出 HTTP 请求时,它会出错,导致测试失败。这时,我们可以引入 Mirage。
让我们在 beforeEach
中导入它并启动它
// homepage-test.js
import { createServer } from "miragejs"
let server
beforeEach(() => {
server = createServer()
})
afterEach(() => {
server.shutdown()
})
it("shows the movies", function () {
cy.visit("/")
cy.get("li.movie").should("have.length", 10)
})
启动 Mirage 后,它会拦截应用程序的网络请求,就像在开发环境中一样。因此,下次运行测试时,您应该会看到以下错误
Mirage: 您的应用程序尝试使用 GET 方法访问 '/api/movies',但没有定义路由来处理此请求
现在我们可以模拟此路由。
import { createServer, Model } from "miragejs"
let server
beforeEach(() => {
server = createServer({
models: {
movie: Model,
},
routes() {
this.namespace = "api"
this.resource("movie")
},
})
})
afterEach(() => {
server.shutdown()
})
it("shows the movies", function () {
cy.visit("/")
cy.get("li.movie").should("have.length", 10)
})
我们的应用程序现在运行,但测试失败。我们可以在控制台中查看日志,可以看到 Mirage 处理了对 /api/movies
的请求,但它没有返回任何数据。
这是因为 Mirage 的数据库是空的。
正如您在指南的先前部分中所学到的,您可以使用 seeds
方法使用工厂和夹具来填充 Mirage 的数据库。但在测试中,我们已经有了一个设置 Mirage 状态的自然位置——测试本身!因此,一般的做法是不使用种子,而是直接在每个测试中设置 Mirage 的数据库状态。
我们可以使用 server.create
和 server.createList
方法直接在测试体中实现
import { createServer, Model } from "miragejs"
let server
beforeEach(() => {
server = createServer({
models: {
movie: Model,
},
routes() {
this.namespace = "api"
this.resource("movie")
},
})
})
afterEach(() => {
server.shutdown()
})
it("shows the movies", function () {
server.createList("movie", 10)
cy.visit("/")
cy.get("li.movie").should("have.length", 10)
})
现在我们通过了测试!
在每个测试之后,Mirage 的服务器都会关闭并重置,因此这些状态不会泄漏到其他测试中。
在开发和测试之间共享您的服务器
在上面的示例中,我们在测试中直接设置了一个新的服务器,但 Mirage 最适合用于将您的模拟服务器定义集中化并在您的开发和测试环境之间共享。毕竟,在生产环境中,您的应用程序会与使用单个 API 契约的真实服务器进行通信!因此,使用单个 Mirage 服务器有助于您在所有使用它的位置维护一致的模拟 API 服务器。
因此,如果您还没有为开发启动 Mirage 服务器,请将您的服务器定义移到一个明确的位置,以便在开发环境和测试中使用它。
└── src
├── App.js
├── App.test.js
└── mirage.js
接下来,导出一个函数,您可以使用它来启动您的 Mirage 服务器。
// src/mirage.js
import { createServer, Model } from "miragejs"
export function startMirage() {
return createServer({
models: {
movie: Model,
},
routes() {
this.namespace = "api"
this.resource("movie")
},
})
}
现在,导入此函数并在您的测试中使用它。
// App.test.js
import { startMirage } from "./mirage"
describe("homepage", function () {
let server
beforeEach(() => {
server = startMirage()
})
afterEach(() => {
server.shutdown()
})
it("shows the movies", function () {
server.createList("movie", 10)
cy.visit("/")
cy.get("li.movie").should("have.length", 10)
})
})
现在您有一个中心位置来定义和更新您的 Mirage 服务器,以及一种简单的方法在您的测试中使用它。
您也可以使用您的 startMirage
函数在开发中启动 Mirage。
// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import { startMirage } from "./mirage"
if (process.env.NODE_ENV === "development") {
startMirage()
}
ReactDOM.render(<App />, document.getElementById("root"))
理想情况下,您应该确保您的 Mirage 代码不会包含在生产环境中(除非您正在构建原型)。如何实现这一点将取决于您的构建工具设置。将来,我们将添加更多涵盖此内容的指南。
The test
环境
Mirage 有一个 environment
选项,默认值为 development
。在开发环境中,Mirage 的延迟为 50ms
(可以自定义),将所有响应记录到控制台,并加载开发 seeds
。
Mirage 也可以置于 test
环境中,该环境将从延迟 0 开始(以保持测试速度)并抑制所有日志记录(以免弄脏您的 CI 服务器日志)。它还将忽略 seeds()
函数,以便数据仅用于开发,不会泄漏到您的测试中或影响您的测试。这有助于保持您的测试确定性。
要在我们的测试中使用 test
环境,让我们更新我们的 startMirage
函数以接受环境选项,我们将默认值为 development
// src/mirage.js
import { createServer, Model } from "miragejs"
- export function startMirage() {
+ export function startMirage({ environment: 'development' }) {
return createServer({
+ environment,
models: {
movie: Model,
},
routes() {
this.namespace = "api"
this.resource("movie")
},
})
}
现在,在我们的测试中,我们可以传入 test
作为环境。我们的测试将无延迟运行,我们不会收到任何 seeds()
,我们也不会看到任何日志。
// src/App.test.js
import { startMirage } from "./mirage"
describe("homepage", function() {
let server
beforeEach(() => {
- server = startMirage()
+ server = startMirage({ environment: 'test' })
})
afterEach(() => {
server.shutdown()
})
it("shows the movies", function() {
server.createList("movie", 10)
cy.visit("/")
cy.get("li.movie").should("have.length", 10)
})
})
如果您发现自己在调试测试并希望查看进出 Mirage 的网络请求,您可以通过将 server.logging
选项设置为 true
来在该测试中启用日志记录。
it("shows the movies", function () {
server.logging = true // enable logs for this test while debugging
server.createList("movie", 10)
cy.visit("/")
cy.get("li.movie").should("have.length", 10)
})
完成后将其删除,以保持 CI 日志的整洁。
保持测试的重点
工厂在将与测试相关的代码尽可能靠近该测试方面很重要。在上面的示例中,我们想验证用户是否会看到十部电影,前提是这些电影存在于服务器上。因此,server.createList('movie', 10)
调用直接在测试中。
假设我们想测试当用户访问标题为“星际穿越”的电影的详细信息路由时,他们是否会在 <h1>
标签中看到该标题。一种实现方法是在服务器的 Movie
工厂中硬编码标题。
// src/mirage.js
import { createServer, Model, Factory } from "miragejs"
export function startMirage({ environment: 'development' }) {
return createServer({
environment,
models: {
movie: Model,
},
factories: {
movie: Factory.extend({
title: 'Interstellar'
})
},
routes() {
this.namespace = "api"
this.resource("movie")
},
})
}
这种方法的问题是我们刚刚对共享的 Mirage 服务器进行了更改,而这种更改与这个测试非常具体。
假设另一个测试需要验证具有不同标题的电影的其他内容。更改工厂以适应这种情况将破坏此测试。
因此,您应该使用 create
和 createList
来覆盖模型的特定属性。这将使与测试相关的代码靠近您的测试,而不会使您的其余测试套件变得脆弱。
// App.test.js
let server
beforeEach(() => {
server = startMirage({ environment: "test" })
})
afterEach(() => {
server.shutdown()
})
it("shows all the movies", function () {
server.createList("movie", 10)
cy.visit("/")
cy.get("li.movie").should("have.length", 10)
})
it("shows the movie's title on the detail route", function () {
let movie = this.server.create("movie", {
title: "Interstellar",
})
cy.visit(`/${movie.id}`)
cy.get("h1").should("contain", "Interstallar")
})
在这两个测试中,我们直接在测试中创建所有相关数据。
第一个测试调用 server.createList('movie', 10)
并且没有指定任何属性覆盖,因为每个电影的具体细节与测试的断言无关。
第二个测试使用 server.create
以及特定的标题,因为测试验证标题是否显示在 UI 中。您还可以看到此测试正在使用 Mirage 自动生成的 ID 来访问特定电影的动态 URL。
当然,有时您会希望设置一些在多个测试中共享的数据,或者在开发和测试中使用的数据。我们将在本页面的后面部分进一步介绍。
安排、行动、断言
Mirage 建议使用 安排、操作、断言方法 来编写测试。您有时会听到这种模式被称为 AAA 测试(“三 A 测试”)。
您可以在上面的测试中看到这种结构。
it("shows all the movies", function () {
// ARRANGE
server.createList("movie", 10)
// ACT
cy.visit("/")
// ASSERT
cy.get("li.movie").should("have.length", 10)
})
当然,有时有必要打破这条规则(例如,在测试的开头或中间添加一些额外的断言),但总的来说,您应该努力遵循这种模式。
测试错误
要测试您的应用程序如何响应服务器错误,您可以在测试中直接覆盖路由处理程序。
import { Response } from "miragejs"
it("shows an error if the save attempt fails", function () {
server.post("/questions", () => {
return new Response(500, {}, { errors: ["The database went on vacation"] })
})
cy.visit("/")
.contains("New")
.click()
.get("input")
.type("New question")
.contains("Save")
.click()
cy.get("h2").should("contain", "The database went on vacation")
})
此路由处理程序定义仅在此测试期间有效,因此一旦测试结束,您在 mirage.js
文件中为 POST 到 /questions
定义的任何处理程序将再次使用。
测试中的共享数据种子
当 Mirage 的环境设置为 test
时,seeds()
配置选项将被忽略,因此对其进行的更改不会影响您的其余测试套件。
如果您想在您的开发场景和测试之间或跨多个测试共享一些逻辑,您可以始终创建一个新的纯 JavaScript 模块并在需要的地方导入它。
要在开发期间使用共享模块,请创建模块。
// mirage/scenarios/shared.js
export default function(server) {
server.loadFixtures('countries');
server.createList('event', 10);
});
...在 seeds()
中加载它,以便场景在开发期间运行。
// mirage.js
import sharedScenario from "./scenarios/shared"
createServer({
seeds(server) {
// Load the shared scenario in development
sharedScenario(server)
// Make some development-specific data
server.create("movie", { title: "Interstellar" })
},
})
...然后还在您的测试中(甚至在通用的测试设置函数中)加载它。
import sharedScenario from "./mirage/scenarios/shared"
import { startServer } from "./mirage"
let server
beforeEach(() => {
server = startServer({ environment: "test" })
sharedScenario(server)
})
afterEach(() => {
server.shutdown()
})
it("shows all the movies", function () {
server.createList("movie", 10)
cy.visit("/")
cy.get("li.movie").should("have.length", 10)
})
这些是使用 Mirage 进行应用程序测试的基础知识!接下来,我们来谈谈集成测试和单元测试。