第 9 部分 - 测试
在本节的最后,我们将学习如何使用 Mirage 在各种不同的服务器状态下测试我们的应用程序。
我们的项目已经使用 Jest 和 测试库 进行了设置。 我们还提供了一个 visit(url)
帮助器,它会在给定的 URL 上渲染我们的 Reminders 应用程序。
让我们打开 src/__tests__/app.js
并编写我们的第一个测试。
我们想要验证当没有提醒时,我们的应用程序是否会显示“全部完成!”。 以下是测试代码 - 将其复制到你的项目中
// __tests__/app.js
import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
test("it shows a message when there are no reminders", async () => {
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
现在,在一个新的终端窗口中,运行 yarn test
。 Jest 应该启动一个观察器,每当你进行更改时都会重新运行你的测试。
测试完成第一次运行后,你应该看到一个错误
无法找到包含文本“全部完成!”的元素。
你应该还会看到一个错误,提示“网络请求失败”。
如果你查看调试输出,你甚至会在 DOM 中看到本教程第 1 部分中熟悉的网络错误 UI。
就像在第 1 部分中一样,这是因为我们的应用程序正在进行初始获取到 /api/reminders
,但是没有服务器响应它。 是时候将我们的 Mirage 服务器引入测试了。
让我们导入我们的 makeServer
函数,并在测试开始时运行它
import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
import makeServer from "../server"
test("it shows a message when there are no reminders", async () => {
makeServer()
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
我们的测试仍然失败,但是如果我们滚动查看调试输出,我们不再看到有关网络请求失败的消息。
相反,我们将看到 DOM 正在渲染我们在本教程之前部分的 seeds()
钩子中创建的现有提醒。
这是有道理的,因为我们创建的是与开发时完全相同的 Mirage 服务器,并且我们在该服务器上播种了这五个提醒来帮助我们开发应用程序。
因此,我们希望在这里有一个全新的 Mirage 服务器实例,一个没有数据的实例,以便我们可以验证空状态是否显示“全部完成!”消息。 我们能够实现这一点的一种方法是返回我们的服务器定义并删除所有 seeds()
数据。
但是,有一个更好的方法,不需要更改我们的开发种子:我们可以使用 environment
选项以“测试”模式启动我们的 Mirage 服务器。
为了了解它的工作原理,回到你的 server.js
文件中,添加 environment: 'test'
选项
// server.js
import { createServer } from "miragejs"
export default function () {
return createServer({
environment: "test",
// rest of server
})
}
保存更改,测试重新运行后,它应该通过了!
那么,"test"
环境对我们的服务器做了什么? 以下几件事:它将 timing
设置为 0,这样我们的测试就可以快速运行;它隐藏了 Mirage 的日志记录,以便你的测试输出保持干净; 最重要的是,它跳过了 seeds()
钩子。
因此,我们可以重用我们所有的模型、序列化器、工厂和路由,但将 seeds()
数据集与开发模式分开。 在测试中,我们将使用每个测试为我们的服务器设置我们需要的特定测试状态的数据。
对于我们当前的测试,我们实际上希望数据库为空,因此我们只需要启动服务器,而无需创建任何额外数据。 这使 Mirage 能够正确处理 GET API 请求,并返回一个空数据集。 现在我们的“全部完成!”消息断言通过了,这正是我们对这次测试的期望。
现在,如果你切换回你的开发服务器(或者在另一个终端窗口中运行 yarn start
),你会注意到我们在 seeds()
中创建的 Reminders 不再显示了。 这是因为我们的开发服务器现在也以测试模式运行。
让我们通过更新从 server.js
导出的函数来修复它,该函数将接受一个环境参数,我们将使用它来设置我们服务器的环境
export default function (environment = "development") {
return createServer({
environment,
// ...rest of server
})
}
现在,我们的开发服务器再次使用我们的种子,并且具有一个人为的延迟和控制台日志来帮助我们进行开发,但我们的测试应该再次失败。
回到我们的测试文件中,我们将更新我们对 makeServer
的调用,并传入一个“测试”环境
import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
import makeServer from "../server"
test("it shows a message when there are no reminders", async () => {
makeServer("test")
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
现在我们的测试通过了,但我们的开发环境仍然有自己的独立种子数据来帮助我们进行开发!
这只是设置 Mirage 的一种方法。 在你自己的项目中,如何设置参数以及选择默认值由你决定。 但关键是,你应该在开发和测试环境中共享你的 Mirage 服务器。
现在,我们已经有了在测试中创建独立 Mirage 服务器的简单方法,让我们继续看看如何为服务器上已经存在三个 Reminders 时编写 UI 测试。
我们将从复制粘贴我们之前的测试并更新描述开始
test("it shows existing reminders", async () => {
makeServer("test")
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
当你保存后,Jest 将运行这两个测试,你应该会看到以下警告
你在已经有另一个 Pretender 实例运行的情况下创建了第二个 Pretender 实例。
Mirage 在幕后使用 Pretender 库,Pretender 告诉我们有两个服务器正在发生冲突。我们需要使用 server.shutdown()
方法清理之前的测试,以及这个新测试的结束。
我们可以先打开 server.js
文件,并确保我们的 makeServer
函数返回服务器实例。
// server.js
export default function (environment = "development") {
return createServer({
// rest of server
})
}
然后回到我们的测试中,我们可以将 makeServer
的返回值赋给一个局部变量,并使用它在每个测试结束时调用 server.shutdown()
。
test("it shows a message when there are no reminders", async () => {
let server = makeServer("test")
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
server.shutdown()
})
test("it shows existing reminders", async () => {
let server = makeServer("test")
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
server.shutdown()
})
现在两个测试都应该在运行并通过,不再出现 Pretender 警告。
现在让我们实际编写第二个测试。
由于我们想要测试服务器从三个提醒开始时 UI 的行为,我们需要在调用 visit('/')
之前创建这些数据。
我们可以使用新的局部 server
变量直接在我们的测试中为服务器播种,就像我们在 seeds()
挂钩中所做的那样。实际上,让我们复制那三个 server.create
语句并将它们带入我们的测试。
test("it shows existing reminders", async () => {
let server = makeServer("test")
server.create("reminder", { text: "Walk the dog" })
server.create("reminder", { text: "Take out the trash" })
server.create("reminder", { text: "Work out" })
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
你应该会看到一个测试失败。
无法找到包含文本“全部完成!”的元素。
如果你查看调试输出,你应该会在 HTML 中看到我们的三个提醒。
这正是我们想要的!
让我们更新我们的断言。
test("it shows existing reminders", async () => {
let server = makeServer("test")
server.create("reminder", { text: "Walk the dog" })
server.create("reminder", { text: "Take out the trash" })
server.create("reminder", { text: "Work out" })
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("Walk the dog")).toBeInTheDocument()
expect(screen.getByText("Take out the trash")).toBeInTheDocument()
expect(screen.getByText("Work out")).toBeInTheDocument()
server.shutdown()
})
有了这个,我们的两个测试现在都通过了!
如你所见,每个测试都为我们提供了一个隔离的地方,以便为了测试的场景而改变 Mirage 服务器的状态。由于我们在每个测试后进行清理,因此这些更改不会从一个测试泄漏到另一个测试。
让我们快速重构一下。由于每个测试都会启动和停止我们的基本 Mirage 服务器,我们可以使用 Jest 的 beforeEach
和 afterEach
挂钩来清理它。
import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
import makeServer from "../server"
let server
beforeEach(() => {
server = makeServer("test")
})
afterEach(() => {
server.shutdown()
})
test("it shows a message when there are no reminders", async () => {
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("All done!")).toBeInTheDocument()
})
test("it shows existing reminders", async () => {
server.create("reminder", { text: "Walk the dog" })
server.create("reminder", { text: "Take out the trash" })
server.create("reminder", { text: "Work out" })
visit("/")
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
expect(screen.getByText("Walk the dog")).toBeInTheDocument()
expect(screen.getByText("Take out the trash")).toBeInTheDocument()
expect(screen.getByText("Work out")).toBeInTheDocument()
})
保存后,两个测试都应该再次通过。
此更改确保我们的服务器始终被清理,并且它还让我们编写专注于与我们正在验证的真实世界用户故事相关的更高级步骤的测试:*假设*服务器上存在三个提醒,*当*用户访问应用程序时,*那么*他们希望在页面上看到它们。
让我们再编写一个测试。我们将测试可以为特定列表创建一个新的提醒。
我们将通过为 Mirage 服务器播种一个列表来开始我们的测试,然后我们将访问该列表的 URL。
test("it can add a reminder to a list", async () => {
let list = server.create("list")
visit(`/${list.id}`)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
})
请注意,在测试中使用来自 Mirage 的数据有多么有用。如果你想了解此时渲染输出,你可以使用 ?open
查询参数以确保应用程序的侧边栏处于打开状态,并调用 screen.debug()
以查看输出中的列表。
test("it can add a reminder to a list", async () => {
let list = server.create("list")
visit(`/${list.id}?open`)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
screen.debug()
})
你应该在侧边栏 UI 中看到 "全部" 和 "列表 0"。
现在,如果我们从这个 URL 创建一个新的提醒,它应该与这个列表相关联,就像我们在第 7 部分的开发过程中所做的那样。
让我们逐步进行。首先将 userEvent
导入到文件顶部。
import userEvent from "@testing-library/user-event"
然后使用它来点击并键入相应的元素。我们使用 data-testid
属性来标识它们。
test("it can add a reminder to a list", async () => {
let list = server.create("list")
visit(`/${list.id}`)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
userEvent.click(screen.getByTestId("add-reminder"))
await userEvent.type(screen.getByTestId("new-reminder-text"), "Work out")
userEvent.click(screen.getByTestId("save-new-reminder"))
// assert something
})
现在,在点击提交按钮后我们应该做什么?
如果你切换回开发并尝试创建提醒,你会看到文本框会淡出,然后在创建提醒后隐藏。
因此在我们的测试中,我们可以等待输入消失,然后断言新的提醒出现在列表中。
test("it can add a reminder to a list", async () => {
let list = server.create("list")
visit(`/${list.id}`)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
userEvent.click(screen.getByTestId("add-reminder"))
await userEvent.type(screen.getByTestId("new-reminder-text"), "Work out")
userEvent.click(screen.getByTestId("save-new-reminder"))
await waitForElementToBeRemoved(() => screen.getByTestId("new-reminder-text"))
expect(screen.getByText("Work out")).toBeInTheDocument()
})
它奏效了!
作为最后一步,断言 Mirage 服务器的状态通常很有意义,只是为了让你对前端代码的行为更加自信。
在我们的例子中,如果一切都正常,我们应该在 Mirage 的数据库中有一个新的提醒,并且它应该与我们创建的列表相关联。我们可以很容易地将这些断言添加到 UI 断言旁边。
test("it can add a reminder to a list", async () => {
let list = server.create("list")
visit(`/${list.id}`)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
userEvent.click(screen.getByTestId("add-reminder"))
await userEvent.type(screen.getByTestId("new-reminder-text"), "Work out")
userEvent.click(screen.getByTestId("save-new-reminder"))
await waitForElementToBeRemoved(() => screen.getByTestId("new-reminder-text"))
expect(screen.getByText("Work out")).toBeInTheDocument()
expect(server.db.reminders.length).toEqual(1)
expect(server.db.reminders[0].listId).toEqual(list.id)
})
你可以将此视为一种简单的方法,用来验证你的 UI 是否正在发送正确的 JSON 负载,而无需降低到断言 HTTP 请求和响应数据的较低级别。
哇,这是迄今为止教程中最长的一步,但你已经取得了很多成就!你有四个测试涵盖了应用程序的一些重要功能,并且你能够重用你的 Mirage 服务器,只对每个测试所需的更改进行更改。
希望你能够看到,当你能够利用现有的 Mirage 服务器,并且只需要调整必要的更改来使服务器进入每个测试的特定状态时,编写测试会多么愉快。
要点
- Mirage 使得在开发和测试之间共享模拟服务器变得容易。
- 在测试时为你的 Mirage 服务器使用
test
环境,这样你的测试运行速度很快,数据库从空状态开始。 - 利用你的 Mirage 服务器在你的测试中很容易访问这一优势,以执行以下操作,例如访问动态 URL 或断言对服务器数据库所做的更改。