本章涵盖
设置测试 React 应用程序的环境
不同 React 测试工具的概述
为 React 应用程序编写第一个测试
当您拥有一流的搅拌机时,与其浪费时间搅拌鸡蛋和糖,您还可以专注于改进工艺的其他方面,例如改进您的食谱或装饰您的蛋糕。
类似于精湛的设备如何让糕点厨师专注于他们工艺的更重要方面,前端框架和库,如 React,让您可以专注于编写 Web 应用程序的更重要方面。与其专注于操作 DOM(自己删除、插入和更新元素),您还可以专注于应用程序的可用性、可访问性和业务逻辑。
在本章中,您将学习如何测试 React 应用程序,以及它与您在测试前端应用程序中学到的知识之间的关系。您将了解像 JSDOM 这样的工具如何在不同的上下文中仍然有用,并且您将学习有助于测试您可能使用的任何其他前端框架的概念。
我之所以选择使用 React 编写这些示例,主要是因为它很受欢迎。我相信这是你们大多数人已经熟悉的工具。
如果您还不了解 React,阅读它的“入门”指南应该足以让您理解我将使用的示例。本章的主要目标不是教你 React,而是展示有用的原则,无论你将使用什么工具。
我将通过解释 React 应用程序如何在浏览器中运行来开始本章,然后教您如何在测试环境中模拟这些条件,以便您获得可靠的质量保证。
在设置测试环境的过程中,我将介绍多种工具。我将解释它们的作用以及它们的工作原理,以便您可以更轻松地解决测试和测试基础架构中的问题。
一旦你的 React 测试环境运行起来,你就会亲眼看到你在测试纯 JavaScript 应用程序时学到的概念中有多少仍然适用。
然后,我将概述 React 测试生态系统中可用的工具,重点介绍我选择的库 react-testing-library,我将用它来向您展示如何为 React 应用程序编写第一个测试。
尽管我将专注于一个库,但我会解释有多少其他库可以工作,它们的优缺点,以及在选择工具时要考虑的因素,以便您可以根据您所在的项目做出自己的决定。重新工作。
7.1 搭建 React 测试环境
了解厨房最重要的事情之一就是一切都在哪里。在路易斯的厨房里,他为每个抽屉贴上标签,并对存放原料和设备的位置制定了严格的规定。他的员工可能认为他太有条理了,但路易斯更愿意称自己为功利主义者。他知道如果找不到平底锅,就无法烤蛋糕。
在本节中,您将设置一个 React 应用程序及其测试环境。在此过程中,您将了解 React 的工作原理以及如何在您的测试环境中重现它。
您将在本章中编写的应用程序将与您在前一章中构建的应用程序一样。不同的是,这次你将使用 React 而不是自己操作 DOM。
因为我无法在不解释浏览器如何运行 React 应用程序的情况下描述 Jest 将如何与您的应用程序交互,所以我将本节分为两部分。在第一部分中,您将了解在浏览器中运行 React 应用程序涉及哪些工具。在第二部分中,您将为应用程序设置测试环境,并了解用于使应用程序在浏览器中运行的工具如何影响测试环境的设置。
7.1.1 设置 React 应用程序
要了解如何测试 React 应用程序,您必须了解使其在浏览器中运行的必要条件。只有这样,您才能在测试中准确地复制浏览器的环境。
在本小节中,您将学习如何配置 React 应用程序以使其在浏览器中运行。在此过程中,我将解释您需要哪些工具以及每个工具的作用。
首先创建一个 index.html,其中包含一个节点,您将在其中渲染应用程序。该文件还应加载一个名为 bundle.js 的脚本,该脚本将包含您应用程序的 JavaScript 代码。
Inventory
❶
❶ 加载 bundle.js 文件,该文件将包含整个捆绑的应用程序
在开始使用任何 JavaScript 包之前,请使用 npm init -y 创建一个 package.json 文件。 您将使用它来跟踪您的依赖项并编写 NPM 脚本。
创建 package.json 文件后,安装编写可在浏览器中运行的 React 应用程序所需的两个主要依赖项:react 和 react-dom。 React 是处理组件本身的库,而 react-dom 允许您将这些组件渲染到 DOM。 请记住将这些包安装为依赖项,使用 npm install --save react react-dom。
作为应用程序的入口点,创建一个 bundle.js 文件。 在其中,您将使用 React 定义一个 App 组件,并使用 react-dom 将该组件的实例渲染到 index.html 中的 app 节点,如下所示。
const ReactDOM = require("react-dom");
const React = require("react");
const header = React.createElement( ❶
"h1",
null,
"Inventory Contents"
);
const App = React.createElement("div", null, header); ❷
ReactDOM.render(App, document.getElementById("app")); ❸
❶ 创建一个 h1 元素作为页面的标题
❷ 创建一个 div 元素,它的唯一子元素是 header 元素
❸ 将 App 元素渲染到 id 为 app 的节点
您可能还记得上一章的内容,因为我们要导入其他库,所以必须将它们捆绑到 index.html 将加载的 bundle.js 文件中。 要执行捆绑,请使用 npm install --save-dev browserify 将 browserify 作为开发依赖项安装。 然后,将以下脚本添加到您的 package.json 文件中,以便您可以使用 npm run build 生成包。
{
"name": "my_react_app",
// ...
"scripts": {
"build": "browserify index.js -o bundle.js" ❶
}
// ...
}
❶ 一个脚本,当你运行 npm run build 时,它会将你的应用程序打包到一个 bundle.js 文件中
生成一个 bundle.js 文件,并使用 npx http-server ./ 在 localhost:8080 为您的应用程序提供服务。
如果您已经熟悉 React,那么现在您可能会想,“但是,嘿,这不是我编写 React 应用程序的方式!” 你是完全正确的。 绝大多数编写 React 应用程序的人都在他们的 JavaScript 代码中使用标记。 他们使用的是所谓的 JSX,一种混合了 JavaScript 和 HTML 的格式。 你习惯看到的 React 代码可能看起来更像这样。
const ReactDOM = require("react-dom");
const React = require("react");
const App = () => { ❶
return (
Inventory Contents
);
};
ReactDOM.render( , document.getElementById("app")); ❷
❶ 将呈现一个 div 的 App 组件,包括一个标题
❷ 将 App 组件渲染到 id 为 app 的 DOM 节点
能够在你的 JavaScript 代码中嵌入标记使组件更具可读性和更少的复杂性,但值得注意的是浏览器不知道如何运行 JSX。因此,要让这些文件在浏览器中运行,您必须使用工具将 JSX 转换为纯 JavaScript。
JSX 是一种更方便的组件编写方式,但它不是 React 的一部分。它扩展了 JavaScript 的语法,当将 JSX 转换为纯 JavaScript 时,它会转换为函数调用。在 React 的情况下,这些函数调用恰好是 React.createElement 调用——与我们在之前的 index.js 文件中的函数调用相同。
JSX 并不是 React 独有的。其他库,例如 Preact,也可以利用 JSX。不同的是,你为 Preact 应用编写的 JSX 需要转换成不同的函数调用,而不是 React.createElement,这是 React 特有的。
一旦我上面编写的 index.jsx 文件转换为纯 JavaScript 文件,其输出应该类似于直接使用 React.createElement 的 index.js 版本产生的输出。
了解 JSX 和 React 的工作方式至关重要,因为这将帮助您设置测试环境。这些知识将使您能够掌握 Jest 在处理 JSX 文件时的作用。
重要 JSX 只是一种更方便的编写组件的方式。浏览器不能运行 JSX 文件。为了能够运行使用 JSX 编写的应用程序,您需要将 JSX 转换为纯 JavaScript。
JSX 不是 React 的独有特性;它是 JavaScript 语法的扩展,在 React 的情况下,它被转换为 React.createElement 调用。
请记住,当您的应用程序进入浏览器时,它就变成了“只是 JavaScript”。
现在您了解了 JSX 的工作原理,是时候看看它的实际效果了。将您的 index.js 文件重命名为 index.jsx,并更新其代码,使其使用 JSX 而不是 React .createElement,就像我之前所做的那样。
要转换您的代码使其可以在浏览器中运行,您将使用 babelify 和 Babel。 babelify 包使 Browserify 能够使用 JavaScript 编译器 Babel 来编译您的代码。然后,您可以使用诸如 preset-react 之类的包将 JSX 转换为纯 JavaScript。鉴于您只在开发环境中需要这些包,请使用 npm install --save-dev babelify @babel/core @babel/preset-react 将它们安装为开发依赖项。
注意我选择在这些示例中使用 Browserify,因为它使它们更加简洁。目前,许多读者可能正在使用 Webpack。
如果你使用 Webpack,同样的基本原理也适用。使用 Webpack,您仍将使用 Babel 及其预设将您的 JSX 代码转换为可以在浏览器中运行的纯 JavaScript。
为了帮助您理解这些工具之间的关系,可以将 Webpack 视为等效于 Browserify,将 babel-loader 视为等效于 babelify。当然,这些比较是一种简化,但在本章的上下文中,它们将帮助您理解示例的工作原理。
更新您的 package.json 以便您的构建命令使用 index.jsx 而不是 index.js 作为您的应用程序的入口点,并为 Browserify 添加配置以便它在构建您的应用程序时使用 babelify 和 @babel/preset-react。
{
"name": "2_transforming_jsx",
"scripts": {
"build": "browserify index.jsx -o bundle.js"
},
// ...
"browserify": {
"transform": [
[
"babelify",
{ "presets": [ "@babel/preset-react" ] } ❶
]
]
}
}
❶ 配置 Browserify 的 babelify 插件,将 JSX 转换为纯 JavaScript
进行此更改后,您的应用程序将准备好在浏览器中运行。 当您运行 npm run build 时,Browserify 会将您的应用程序捆绑到一个纯 JavaScript 文件中。 在打包过程中,它会通过 babelify 与 Babel 交互,将 JSX 转化为纯 JavaScript,如图 7.1 所示。
最后,当您使用 npx http-server ./ 为您的应用程序提供服务时,index.html 将加载 bundle.js 文件,该文件会将 App 组件挂载到页面上。
要查看您的应用程序是否正常工作,请使用 npm run build 构建它,并使用 npx http-server ./ 为其提供服务,以便能够在 localhost:8080 访问它。
您可能已经注意到的另一个细节是,到目前为止,我一直在本书的示例中使用 Node.js 的 require 来导入 JavaScript 模块。 但是,此函数不是在 JavaScript 中导入模块的标准方法。 在本章中,我将使用 ES 导入而不是使用 require。
import ReactDOM from "react-dom";
import React from "react";
// ...
为了能够使用 ES 导入,您必须使用 Babel 的预设环境将 ES 导入转换为 require 调用——也称为 CommonJS 导入。
注意 在撰写本文时,Node.js 的最新版本已经支持 ES 导入。 我选择演示如何使用 Babel 做到这一点,以便使用 Node.js 以前版本的读者也可以跟上。 你可以在 https://nodejs.org/api/esm.html 阅读更多关于 Node.js 对 ES 模块的支持。
使用 npm install --save-dev @babel/preset-env 安装 Babel 的预设环境作为开发依赖项,并更新你的 package.json 以便它在构建应用程序时使用这个包。
{
"name": "2_transforming_jsx",
// ...
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
"@babel/preset-env", ❶
"@babel/preset-react"
]
}
]
]
}
}
❶ 配置 Browserify 的 babelify 插件来转换你的代码,这样你就可以针对特定环境而无需微观管理特定于环境的更改
现在您已经完成了 babelify 的设置,您可以像以前一样构建和访问您的应用程序。 首先,运行 npm run build,然后在 localhost:8080 使用 npx http-server 提供它。/
7.1.2 搭建测试环境
现在您已经了解了在浏览器中运行 React 应用程序所涉及的内容,是时候了解在 Node.js 中运行所涉及的内容,以便您可以使用 Jest 来测试它。
通过使用 npm install --save-dev jest 将 Jest 作为开发依赖项安装来开始设置测试环境。
因为你将开始测试你的 App 组件,所以将它分离到它自己的 App.jsx 文件中并更新 index.jsx 以便它从 App.jsx 导入 App。
import React from "react";
export const App = () => { ❶
return (
Inventory Contents
);
};
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx"; ❶
ReactDOM.render( , document.getElementById("app")); ❷
❶ 从 App.jsx 导入 App 组件
❷ 渲染一个 App 实例到 ID 为 app 的 DOM 节点
现在,在您甚至可以尝试呈现 App 并对其进行测试之前(我们将在下一节中进行),您必须能够执行其 App.jsx 文件中的代码。
创建您的第一个测试文件,并将其命名为 App.test.js。 现在,只需尝试使用 ES Modules 语法导入 App。
import { App } from "./App.jsx";
尝试使用 Jest 运行此测试文件时,您将收到语法错误。
提示要运行您的测试,请更新您的 package.json,并添加一个名为 test 的 NPM 脚本来调用 jest,就像我们之前所做的那样。这个 NPM 脚本将允许你使用 npm test 运行你的测试。
在撰写本文时,我使用的是 Node.js v12。在这个版本中,即使只是在带有 .js 扩展名的文件中使用 ES 模块语法导入 App 也会导致您的测试失败。
要解决这个问题,您必须使用 Babel 和 preset-env 包转换 App.test.js,就像您之前使用 Browserify 捆绑代码时所做的那样。不同的是,这次你不需要 Browserify 作为中间件。相反,您将指示 Jest 本身使用 Babel。
要告诉 Jest 转换您的文件以便您可以在 Node.js 中运行它们,您可以将 Babel 的配置移动到它自己的 babel.config.js 文件中。在撰写本文时我正在使用的 Jest 版本中,只要拥有这个配置文件就足以让 Jest 知道它应该在运行文件之前转换文件。
继续创建一个 babel.config.js 文件,该文件使用 preset-env 来转换您的源代码,以便它们可以在 Node.js 中运行。
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current" ❶
}
}
]
]
};
❶ 配置 Babel 以转换您的文件,以便它们与 Node.js 兼容,因此可以由在 Node.js 中运行的 Jest 执行
此更改使导入本身成功,但仍不会导致 Jest 无错误退出。 如果您尝试重新运行 Jest,您会看到它现在抱怨在您的 App.jsx 文件中找到了意外的令牌。
发生这个错误是因为,就像浏览器一样,Node 不知道如何执行 JSX。 因此,您必须先将 JSX 转换为纯 JavaScript,然后才能使用 Jest 在 Node.js 中运行它。
更新您的 babel.config.js 文件,以便它使用 preset-react 将 JSX 转换为 React.createElement 调用,就像您之前所做的那样。
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react" ❶
]
};
❶ 配置 Babel 将 JSX 转换为纯 JavaScript,以便您可以使用 Node.js 执行它们
现在您已经创建了一个 .babel.config.js 配置文件,Jest 将在运行之前使用 Babel 对这些文件进行转换,如图 7.2 所示。 Jest 需要这样做,因为它运行在 Node.js 中,Node.js 不知道 JSX,其当前版本还不能处理 ES 模块。 为了将 JSX 转换为纯 JavaScript,它使用了 preset-react,而将 ES 导入转换为 CommonJS 导入(需要调用),它使用了 preset-env。
最后,使用preset-react后,Jest就可以运行App.test.js了。它仍然以错误结束,但现在它更容易解决:您还没有编写任何测试。
重要事项 了解您正在使用的每个工具的作用至关重要。在本章的第一部分,您使用 Browserify 将您的应用程序捆绑到一个 JavaScript 文件中。 babelify 包使您能够使用编译器本身的 Babel 来转换您的文件。 preset-env 和 preset-react 包负责告诉 Babel 如何执行转换。
在最后一部分中,您将 Jest 配置为在运行文件之前使用 Babel 来转换文件。 preset-env 和 preset-react 的作用保持不变:它们告诉 Babel 如何转换代码。
测试 React 应用程序与测试 vanilla 前端应用程序没有太大区别。在这两种情况下,您都希望尽可能准确地在 Node.js 中复制浏览器环境。为此,您可以使用模拟浏览器 API 的 JSDOM 和 Babel 等工具来转换您的代码,使其可以在浏览器中运行。
如果您使用的是 React 以外的库或框架,要了解如何对其进行测试,我建议您遵循我在本节中介绍的相同思维过程。首先,检查您需要做什么才能让您的应用程序在浏览器中运行。了解每个工具的作用及其工作原理。然后,当您对应用程序在浏览器中的工作方式有了很好的了解后,修改这些步骤,使代码能够在 Node.js 中运行,以便您可以使用 Jest。
7.2 React 测试库概述
一流的烤箱、一些优质的法国炊具和全新的搅拌机不会为您烤制蛋糕,但为工作选择合适的工具可以让您成功一半。
在本节中,我将简要概述 React 测试生态系统中可用的工具。我将解释它们的工作原理以及它们的优缺点,以便您可以选择您认为适合您的项目的工具。
通过大量的例子,我将教你如何使用 React 自己的实用程序。我将解释这些实用程序如何与您的测试环境交互以及与测试 React 应用程序相关的基本概念。
因为大多数 React 测试库都是 React 自己的测试工具中已有功能的便捷包装器,对它们的深入理解将使您更容易掌握幕后发生的事情。
一旦您对 React 自己的测试实用程序以及 React 在您的测试环境中的工作方式有了很好的了解,您就会看到可以使用哪些库来减少必须在测试中编写的代码量。
尽管我将解释多个库的优缺点,但我将在本节中关注的库是 react-testing-library。它是我为大多数项目选择的测试库,在本节中,您将学习如何使用它并理解我为什么在大多数情况下推荐它。
7.2.1 渲染组件和 DOM
您在本节中的第一个任务是为 App 组件编写测试。您将编写的测试将遵循三种 As 模式:arrange、act、assert。因为 Jest 已经为您设置了一个 JSDOM 实例,您可以直接跳转到执行和断言。您将向 JSDOM 实例呈现 App 并检查它是否显示正确的标题。
为了能够为 App 组件编写此测试,您必须解决两个问题。首先,您必须能够呈现 App 本身。然后,您必须能够检查 DOM 中是否存在正确的标头。
首先将一个 div 附加到 JSDOM 实例的文档。稍后,您会将 App 组件渲染到此 div,就像您在应用程序代码中在将 App 渲染到 index.html 中的 div 时所做的一样。
import { App } from "./App.jsx";
const root = document.createElement("div"); ❶
document.body.appendChild(container); ❷
❶ 创建一个div元素
❷ 将 div 附加到文档正文
现在,继续编写一个测试,将 App 呈现到您刚刚创建的根节点。 要渲染 App,您将使用 react-dom。
与您在应用程序中所做的不同,在您的测试中,您必须将与组件的每个交互包装到一个名为 act 的 React 测试实用程序中,它是 react-dom 包的一部分。 act 函数确保与您的交互相关的更新已被处理并应用到 DOM,在这种情况下,由 JSDOM 实现。
import React from "react";
import { App } from "./App.jsx";
import { render } from "react-dom";
import { act } from "react-dom/test-utils";
const root = document.createElement("div");
document.body.appendChild(root); ❶
test("renders the appropriate header", () => {
act(() => {
render( , root); ❷
});
});
❶ 将一个 div 附加到文档正文
❷ 将 App 的一个实例渲染到您附加到文档正文的 div
注意因为您的测试文件现在使用 JSX 来创建应用程序,我建议您将其扩展名更改为 .jsx 以指示其包含的代码类型。
在您刚刚编写的测试中,您准确地模拟了浏览器如何呈现 App 组件。您不是用测试替身替换 React 的任何部分,而是利用 JSDOM 的 DOM 实现来渲染组件,就像浏览器一样。
除了使测试更可靠之外,将组件渲染到 DOM 还使您能够使用任何适用于普通 JavaScript 应用程序的测试工具和技术。只要您将 HTML 元素呈现到 DOM,您就可以像在任何其他情况下一样与这些元素进行交互。
例如,尝试使用文档的 querySelector 函数来查找呈现的标题,并对其 textContent 属性进行断言,就像您对任何其他 DOM 节点所做的一样。
// ...
test("renders the appropriate header", () => {
act(() => {
render( , root); ❶
});
const header = document.querySelector("h1"); ❷
expect(header.textContent).toBe("Inventory Contents"); ❸
});
❶ 将 App 的一个实例渲染到文档正文中的 div
❷ 查找文档中的 h1 元素
❸ 断言头部的内容是“Inventory Contents”
您刚刚编写的测试使用 react-dom/test-utils 将 App 渲染到 JSDOM 实例,然后使用 Web API 查找和检查 h1 元素,以便您可以对其进行断言。 这些测试的步骤如图 7.3 所示。
您使用 react 或 react-dom 的事实对于您刚刚使用的 document.querySelector 函数是完全透明的。此函数对文档的元素进行操作,无论它们是如何呈现的。
相同的原则适用于其他测试实用程序。鉴于您将 App 组件渲染到 DOM,您可以使用 dom-testing-library 等 DOM 测试实用程序来使您的测试更具可读性和健壮性。
注意一般来说,在处理 React 时,由于集成层很薄,我认为我的组件是原子“单元”。因此,我将隔离组件的测试归类为“单元测试”。当测试涉及多个组件时,我更愿意称其为“集成测试”。
尽管我将你编写的最后一个测试归类为单元测试,但也有人会争辩说它应该被标记为集成测试,因为你不仅在测试代码,还测试它是否与 React 正确交互。
特别是在这种情况下,将测试金字塔视为一个连续的频谱,而不是一组离散的类别是很有趣的。例如,即使我将这个测试放在金字塔的底部,它仍然高于调用函数并检查返回值的测试。
在测试组件时使用 React。不要将您的组件与 React 隔离,以便您可以将测试标记为“单元测试”。
孤立地测试你的组件会让你的测试几乎毫无用处,因为 React 渲染和更新组件的逻辑是让你的应用程序工作的一个组成部分。
请记住,您的目标不是标记测试。您的目标是在每个不同的集成级别编写正确数量的测试。
继续并使用 npm install --save-dev @testing-library/dom 安装 dom-testing-library 作为开发依赖项。然后,尝试使用该库的屏幕对象的 getByText 方法而不是文档的 querySelector 函数查找页面的标题。
// ...
import { screen } from "@testing-library/dom";
// ...
test("renders the appropriate header", () => {
act(() => {
render( , root); ❶
});
expect(screen.getByText("Inventory Contents")) ❷
.toBeTruthy();
});
❶ 将 App 渲染到您附加到文档正文的 div
❷ 使用@testing-library/dom 中的getByText 函数查找内容为“Inventory Contents”的元素,然后断言该元素已找到
现在您已经安装了 dom-testing-library,您还可以使用它的 fireEvent API。 就 fireEvent 而言,它像处理任何其他节点一样处理 DOM 节点,因此,它可以单击按钮、填充输入和提交表单,就像在任何其他情况下一样。
就像 dom-testing-library 不关心 React 一样,React 和 react-dom 也不关心它们是在浏览器中呈现还是在 JSDOM 中呈现。 只要 JSDOM API 与浏览器的匹配,React 就会以相同的方式响应事件。
要查看如何使用 dom-testing-library 与组件进行交互,首先,向您的 App 组件添加一个按钮,该按钮可增加可用的芝士蛋糕的数量。
import React from "react";
export const App = () => {
const [cheesecakes, setCheesecake] = React.useState(0); ❶
return (
Inventory Contents
Cheesecakes: {cheesecakes}
);
};
❶ 创建一个代表库存的芝士蛋糕的状态
❷ 当用户点击按钮时增加芝士蛋糕的数量
测试此功能时,请记住,您必须将与组件的交互包装到 react-dom 提供的 act 函数中。 这个函数确保交互已经被处理并且必要的更新已经被应用到 DOM。
// ...
import { screen, fireEvent } from "@testing-library/dom";
// ...
test("increments the number of cheesecakes", () => {
act(() => {
render( , root); ❶
});
expect(screen.getByText("Cheesecakes: 0")).toBeInTheDocument(); ❷
const addCheesecakeBtn = screen.getByText("Add cheesecake"); ❸
act(() => { ❹
fireEvent.click(addCheesecakeBtn);
});
expect(screen.getByText("Cheesecakes: 1")).toBeInTheDocument(); ❺
});
❶ 渲染一个 App 实例
❷ 使用@testing-library/dom 中的getByText 方法查找指示库存包含零个芝士蛋糕的元素,然后断言它存在
❸ 找到按文字添加芝士蛋糕的按钮
❹ 使用来自@testing-library/dom 的 fireEvent 来单击将芝士蛋糕添加到清单中的按钮,并确保更新被处理并应用于 DOM
❺ 使用 getByText 查找指示库存包含一个芝士蛋糕的元素,然后断言它存在
在前面的示例中,也如图 7.4 所示,您使用了 react-dom/utils 中的 render 方法将 App 组件渲染到您的 JSDOM 实例,并使用了 dom-testing-library 中的 getByText 查询来查找页面中的元素并 与他们互动。
现在您已经看到 dom-testing-library 与您的 React 应用程序进行了适当的交互,请尝试像测试纯 JavaScript 项目时一样使用 jest-dom。 因为 jest-dom 在 DOM 之上运行,它将与您正在渲染的 React 组件无缝协作,就像 dom-testing-library 所做的那样。
要使用 jest-dom,请使用 npm install --save-dev @testing-library/jest-dom 安装它,创建一个脚本以使用 jest-dom 提供的断言扩展 Jest,并更新 jest.config.js 以便它 在运行测试文件之前执行该脚本。
module.exports = {
setupFilesAfterEnv: ["/setupJestDom.js"] ❶
};
❶ 在每次测试之前,Jest 执行脚本,该脚本使用来自 jest-dom 的断言扩展 Jest。
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
❶ 使用 jest-dom 中的断言扩展 Jest
一旦你设置了这个库,用它来断言标题当前在文档中。
// ...
test("renders the appropriate header", () => {
act(() => {
render( , root);
});
expect(screen.getByText("Inventory Contents"))
.toBeInTheDocument(); ❶
});
❶ 使用 jest-dom 中的断言来断言某个元素在文档中
这些只是您在测试普通 JavaScript 应用程序和测试 React 应用程序时可以使用的众多工具中的两个。
重要只要您将组件渲染到 DOM 并准确地再现浏览器的行为,任何适用于纯 JavaScript 应用程序的工具都适用于 React 应用程序。
作为一般建议,在研究如何测试使用特定库或框架的应用程序时,我建议您首先了解该库或框架本身如何在浏览器中工作。无论您使用什么库或框架,通过像浏览器一样呈现您的应用程序,您可以使您的测试更可靠并扩大您可以使用的工具范围。
除了编译和渲染步骤之外,测试 React 应用程序类似于测试普通 JavaScript 应用程序。在测试 React 应用程序时,请记住您主要处理 DOM 节点,并且编写有效测试的相同原则仍然适用。例如,您应该编写紧密而精确的断言,并使用构成它应该是的组成部分的属性来查找元素。
7.2.2 React 测试库
到目前为止,因为您正在处理 React,所以您的测试涉及大量特定于 React 的问题。因为您需要将组件渲染到 DOM,所以您需要手动将 div 附加到 JSDOM 实例并使用 react-dom 自己渲染组件。除了这个额外的工作,当你的测试完成时,你没有一个拆卸钩子来从 DOM 中删除渲染的节点。缺少清理例程可能会导致一个测试干扰另一个测试,如果要实现它,则必须手动执行。
此外,为了确保更新将被处理并应用于 DOM,您将与组件的交互包装到 react-dom 提供的 act 函数中。
为了有效地解决这些问题,您可以将 dom-testing-library 替换为 react-testing-library。与 dom-testing-library 中的方法不同,react-testing-library 中的方法已经考虑了 React 特定的问题,例如将交互包装为行为或在测试完成后自动卸载组件。
在本节中,您将学习如何使用 react-testing-library 来测试您的组件。您将编写一个组件,其中包含一个向清单中添加新项目的表单,另一个包含一个包含服务器清单内容的列表。然后,您将学习如何使用 react-testing-library 测试这些组件。
通过使用 react-testing-library 及其方法,您将了解它如何通过隐藏您之前看到的测试 React 应用程序的复杂性和特殊性,使您的测试更加简洁和易于理解。
注意 react-testing-library 包建立在 dom-testing-library 之上,两者都是同一个“系列”工具的一部分。 因此,它们的 API 有意地几乎相同。
渲染组件和查找元素
您使用 react-testing-library 的第一个任务是使用它来将组件渲染到 DOM。 在整个过程中,我将解释使用 react-testing-library 和 react-dom 之间的区别。
使用 npm install --save-dev @testing-library/react 将 react-testing-library 安装为 dev 依赖项,以便您可以开始使用其功能而不是 dom-testing-library 中的功能。
安装 react-testing-library 后,开始使用它的 render 方法而不是 react-dom 中的方法来渲染组件。
因为 react-testing-library 将自己的容器附加到 DOM 中,它将在其中渲染元素,所以您不需要自己创建任何节点。
// ...
import { render } from "@testing-library/react";
// ...
// Don't forget to delete the lines that
// append a `div` to the document's body.
test("renders the appropriate header", () => {
render( ); ❶
// ...
});
test("increments the number of cheesecakes", () => {
render( ); ❷
// ...
});
❶ 使用 react-testing-library 中的 render 函数将 App 的一个实例渲染到文档中
❷ 使用 react-testing-library 中的 render 函数将 App 的一个实例渲染到文档中
在前面的示例中,您使用 react-testing-library 中的 render 方法替换了 react-dom/test-utils 中的 render 方法。 与 react-dom/test-utils 不同,react-testing-library 会在每次测试后自动配置一个钩子来清理你的 JSDOM 实例。
除了不需要为您的 JSDOM 实例设置或清理例程之外,react-testing-library 的 render 函数返回一个对象,该对象包含与 dom-testing-library 包含的查询相同的查询。 它们之间的区别在于,来自 react-testing-library 的 render 方法的查询会自动绑定在渲染组件内运行,而不是在整个 JSDOM 实例内运行。 由于这些查询的范围是有限的,因此您不必使用 screen 或将容器作为参数传递。
// ...
test("renders the appropriate header", () => {
const { getByText } = render( ); ❶
expect(getByText("Inventory Contents")).toBeInTheDocument(); ❷
});
test("increments the number of cheesecakes", () => {
const { getByText } = render( ); ❸
expect(getByText("Cheesecakes: 0")).toBeInTheDocument(); ❹
const addCheesecakeBtn = getByText("Add cheesecake"); ❺
act(() => { ❻
fireEvent.click(addCheesecakeBtn);
});
expect(getByText("Cheesecakes: 1")).toBeInTheDocument(); ❼
});
❶ 使用 react-testing-library 中的 render 函数将 App 的一个实例渲染到文档中,并获得一个作用域为渲染结果的 getByText 查询
❷ 使用作用域 getByText 函数通过文本查找元素,然后断言它在文档中
❸ 再次使用 react-testing-library 中的 render 函数来渲染 App 的一个实例,并获得一个作用域为渲染结果的 getByText 查询
❹ 使用作用域内的 getByText 函数查找表明库存中没有芝士蛋糕的元素,然后断言它在文档中
❺ 使用作用域内的 getByText 函数查找将芝士蛋糕添加到库存中的按钮
❻ 使用来自 dom-testing-library 的 fireEvent 点击按钮将芝士蛋糕添加到库存中
❼ 最后一次使用作用域内的 getByText 查找指示库存现在包含一个芝士蛋糕的元素,然后断言该元素在文档中
感谢 react-testing-library,渲染组件变得更加简洁。由于 react-testing-library 处理安装和卸载组件,因此您无需手动创建特殊节点,也无需设置任何清理挂钩。
此外,您的查询变得更加安全,因为 react-testing-library 中的渲染方法将您的查询范围限定到渲染组件的根容器。因此,在执行断言时,您可以保证对被测组件中的元素进行断言。
与组件交互
以前,为了与您的应用程序交互,您使用了 dom-testing-library 中的 fireEvent 实用程序,并结合了对 React 的 act 函数的调用。尽管这两个工具使您能够以编程方式与组件执行丰富的交互,但 react-testing-library 为您提供了一种更短的方法。
在本小节中,您将创建一个组件,其中包含一个表单,供 Louis 的员工将新项目添加到面包店的库存中。然后,您将学习如何使用 react-testing-library 与此表单交互,以便您可以编写简洁、可靠且可读的测试。
要使用 react-testing-library 与组件交互,您将使用它的 fireEvent 实用程序,而不是来自 dom-testing-library 的实用程序。两者之间的区别在于 react-testing-library 中的 fireEvent 实用程序已经将交互包装到行为调用中。因为 react-testing-library 负责使用 act,所以你不必自己担心。
继续使用 react-testing-library 中的 fireEvent 函数替换 dom-testing-library 中的 fireEvent 函数,这样您就可以停止使用 act 了。
// ...
// At this stage, you won't need any imports
// from `@testing-library/dom` anymore.
import { render, fireEvent } from "@testing-library/react"; ❶
// ...
test("increments the number of cheesecakes", () => {
const { getByText } = render( ); ❷
expect(getByText("Cheesecakes: 0")).toBeInTheDocument(); ❸
const addCheesecakeBtn = getByText("Add cheesecake"); ❸
fireEvent.click(addCheesecakeBtn); ❹
expect(getByText("Cheesecakes: 1")).toBeInTheDocument(); ❸
❶ 从 react-testing-library 导入 render 和 fireEvent
❷ 将 App 的一个实例渲染到文档中,并获得一个作用域为渲染结果的 getByText 函数
❸ 使用作用域的 getByText 函数来查找 DOM 中的元素,并对其进行断言和交互
❹ 使用 react-testing-library 中的 fireEvent 实用程序单击将芝士蛋糕添加到清单中的按钮,这样您就不必将交互包装到行为调用中
通过使用来自 react-testing-library 的查询以及 render 和 fireEvent 方法,您完全不需要使用 dom-testing-library。 在此更改之后,react-testing-library 是您必须与之交互以呈现组件、查找元素并与它们交互的唯一库,如图 7.5 所示。
提示要卸载 dom-testing-library 并将其从您的依赖项列表中删除,请使用 npm uninstall dom-testing library。
现在您已经了解了 react-testing-library 中的 fireEvent 方法的工作原理,您将创建一个更复杂的组件并学习如何对其进行测试。这个新组件将被称为 ItemForm,它将替换当前增加芝士蛋糕数量的按钮。
类似于上一章应用中的表单,当提交时,它会向服务器发送一个请求。因为它将包含两个字段——一个用于项目名称,另一个用于添加数量——该表单将允许库存经理添加任何数量的任何产品。
注意 因为本章的重点是测试 React 应用程序,而不是后端,所以我将在第 6 章中使用的同一服务器上构建下一个示例。
您可以在本书的 GitHub 存储库中找到服务器的代码以及本章的示例,网址为 https://github.com/lucasfcosta/testing-javascript-applications。
在第 7 章的文件夹中,您将找到一个名为 server 的目录,其中包含一个能够处理您的 React 应用程序将发出的请求的 HTTP 服务器。
要运行该服务器,请导航到其文件夹,使用 npm install 安装其依赖项,使用 npm run migrate:dev 确保您的数据库架构是最新的,并使用 npm start 启动服务器。默认情况下,您的 HTTP 服务器将绑定到端口 3000。
通过创建一个只能管理它自己的状态的 ItemForm 组件来开始处理这个表单。现在不用担心向服务器发送请求。
export const ItemForm = () => {
const [itemName, setItemName] = React.useState(""); ❶
const [quantity, setQuantity] = React.useState(0); ❷
const onSubmit = (e) => {
e.preventDefault();
// Don't do anything else for now
}
return (
);
};
❶ 创建一个状态,用于存储表单的 itemName
❷ 创建一个状态来存储表单的数量
❸ 创建一个包含两个字段和一个提交按钮的表单。此表单在提交时将调用 onSubmit 函数。
作为练习,为了练习您之前学到的关于 react-testing-library 查询的知识,创建一个名为 ItemForm.test.jsx 的文件,并编写一个单元测试来验证该组件是否呈现正确的元素。此测试应呈现 ItemForm 并使用呈现函数返回的查询来查找您想要的元素。然后,您应该断言这些元素存在于 DOM 中,就像您之前在 App 中查找标题一样。
注意您可以在本书的 GitHub 存储库中的 Chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library 文件夹中找到有关如何编写此测试的完整示例,网址为 https://github.com/lucasfcosta/testing-javascript-applications。
现在 ItemForm 呈现一个包含两个字段和一个提交按钮的表单,您将让它在用户提交新项目时向服务器发送请求。
为了确保服务器地址在您的项目中保持一致,请创建一个 constants.js 文件,您将在其中创建一个包含服务器地址的常量并将其导出。
export const API_ADDR = "http://localhost:3000";
最后,更新 ItemForm.js 以便在用户提交表单时向服务器发送请求。
// ...
import { API_ADDR } from "./constants"
const addItemRequest = (itemName, quantity) => { ❶
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
}
export const ItemForm = () => {
const [itemName, setItemName] = React.useState("");
const [quantity, setQuantity] = React.useState(0);
const onSubmit = (e) => { ❷
e.preventDefault();
addItemRequest(itemName, quantity)
}
return (
);
};
❶ 向服务器路由发送请求的函数,该路由处理向库存添加新项目
❷ 一个 onSubmit 函数,用于防止页面重新加载,并在提交表单时向服务器发送请求
❸ 提交时调用 onSubmit 的表单元素
在与组件交互并检查它是否向服务器发送了适当的请求之前,您必须记住将全局 fetch 替换为 isomorphic-fetch,就像在前一章中所做的那样。 否则,您会遇到错误,因为运行 Jest 的 Node.js 没有全局获取功能。
要在运行测试时替换全局 fetch,请使用 npm install --save-dev isomorphic-fetch 将 isomorphic-fetch 作为开发依赖项安装。 然后,创建一个名为 setupGlobalFetch.js 的文件,该文件会将 isomorphic-fetch 的 fetch 函数分配给 JSDOM 窗口中的 fetch 属性。
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch; ❶
❶ 将 isomorphic-fetch 的 fetch 函数赋值给全局窗口的 fetch 属性
创建此文件后,通过更新 jest.config.js 中的 setupFilesAfterEnv 选项,告诉 Jest 在每个测试文件之前运行它。
module.exports = {
setupFilesAfterEnv: [
"/setupJestDom.js",
"/setupGlobalFetch.js" ❶
]
};
❶ 在执行每个测试文件之前,Jest 将运行脚本,该脚本从 isomorphic-fetch 全局窗口的 fetch 属性分配 fetch 函数。
现在您的组件可以在测试期间访问 fetch,您将测试表单是否将适当的请求发送到您的后端。 在本次测试中,您将使用 react-testing-library 中的 fireEvent 函数填写和提交表单,并使用 nock 拦截请求并响应它们。 因为您在 JSDOM 中处理 DOM 节点,并且 fireEvent 已经在 act 函数中执行交互,所以此测试类似于对普通 JavaScript 应用程序的测试。
// ...
import nock from "nock";
import { render, fireEvent } from "@testing-library/react";
const API_ADDR = "http://localhost:3000";
// ...
test("sending requests", () => {
const { getByText, getByPlaceholderText } = render( );
nock(API_ADDR) ❶
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change( ❷
getByPlaceholderText("Item name"),
{ target: {value: "cheesecake"} }
);
fireEvent.change( ❸
getByPlaceholderText("Quantity"),
{ target: {value: "2"} }
);
fireEvent.click(getByText("Add item")); ❹
expect(nock.isDone()).toBe(true); ❺
});
❶ 创建一个拦截器,以 200 状态响应发送到 /inventory/cheesecake 的 POST 请求,其 body 的数量属性为 2
❷ 更新了“cheesecake”,项目名称的表单字段
❸ 更新“2”,即商品数量的表单字段
❹ 点击提交表单的按钮
❺ 期望所有的拦截器都已经到达
完成 ItemForm 的实现后,您将在 App 组件中使用它。 在此更改之后,用户将能够将任意数量的任何物品添加到库存中——不仅仅是芝士蛋糕。
import React from "react";
import { ItemForm } from "./ItemForm.jsx";
export const App = () => {
return (
Inventory Contents
❶
);
};
❶ 在 App 中渲染一个 ItemForm 的实例
为了确保所有测试仍然通过,请记住从 App.test.jsx 中删除验证负责将芝士蛋糕添加到库存的按钮的测试。
为了让您看到表单正常工作,请使用 npm run build 构建您的应用程序,并通过使用 npx http-server ./ 为它提供服务在 localhost:8080 上提供服务。打开开发人员工具的“网络”选项卡,填写表单并提交新项目,以便您可以看到发送到服务器的请求。
等待活动
在编写 React 应用程序时,您最终会发现依赖外部源导致组件更新的情况。例如,您可以拥有一个依赖于生成随机值的计时器或响应请求的服务器的组件。
在这些情况下,您需要等待这些事件发生并等待 React 处理更新并将最新组件呈现到 DOM。
在本节中,您将实现并测试一个 ItemList 组件,该组件从服务器获取项目列表并更新自身以显示库存。没有这个清单,员工就不可能管理面包店的库存。
通过创建一个名为 ItemList.jsx 的文件并编写将列出库存的组件来开始实现此功能。 ItemList 组件应该接收一个 itemsprop 并使用它来呈现项目列表。
import React from "react";
export const ItemList = ({ items }) => { ❶
return (
{Object.entries(items).map(([itemName, quantity]) => { ❷
return (
-
{itemName} - Quantity: {quantity}
);
})}
);
};
❶ 创建一个可以接收items prop的ItemList组件
❷ 遍历 items 中的每个属性,并为每个属性渲染一个带有名称和数量的 li 元素
要验证此组件是否充分呈现传递给它的项目列表,您将在 ItemList.test.jsx 中编写一个测试。 此测试应将包含几个项目的对象传递给 ItemList,使用 react-testing-library 中的 render 函数将组件渲染到 DOM,并检查列表是否包含正确的内容。
import React from "react";
import { ItemList } from "./ItemList.jsx";
import { render } from "@testing-library/react";
test("list items", () => {
const items = { cheesecake: 2, croissant: 5, macaroon: 96 }; ❶
const { getByText } = render( ); ❷
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3); ❸
expect(getByText("cheesecake - Quantity: 2")) ❹
.toBeInTheDocument();
expect(getByText("croissant - Quantity: 5")) ❺
.toBeInTheDocument();
expect(getByText("macaroon - Quantity: 96")) ❻
.toBeInTheDocument();
});
❶ 创建一个静态的项目列表
❷ 使用静态项目列表呈现 ItemList 元素
❸ 期望渲染的 ul 元素有三个子元素
❹ 找到一个元素表明库存包含 2 个芝士蛋糕
❺ 找到一个元素表示库存包含 5 个羊角面包
❻ 找到一个元素,表明库存包含 96 个杏仁饼
现在您知道 ItemList 可以充分呈现库存的项目,您将使用服务器提供给您的内容填充它。
要首次填充 ItemList,请通过在呈现时向 GET /inventory 发送请求来使 App 获取库存的内容。 一旦客户端收到响应,它应该更新它的状态并将项目列表传递给 ItemList。
import React, { useEffect, useState } from "react";
import { API_ADDR } from "./constants"
// ...
export const App = () => {
const [items, setItems] = useState({});
useEffect(() => { ❶
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`)
setItems(await response.json()); ❷
};
loadItems();
}, []);
return (
Inventory Contents
❸
);
};
❶ 使 App 组件在呈现时向服务器发送请求以获取项目列表
❷ 当收到来自服务器的项目列表时,更新 App 内的状态
❸ 使用从服务器获取的物品列表渲染物品列表
注意 在打包应用程序时,由于在 useEffect 钩子中使用了 async/await,您必须配置 Babel 的 preset-env 以使用来自名为 core-js 的包中的 polyfill。 否则,您的应用程序将无法在浏览器中运行,即使在构建之后也是如此。
为此,请使用 npm install --save-dev core-js@2 安装 core-js,并在 package.json 中更新 Browserify 的转换设置。
core-js 包包含用于更新 ECMAScript 版本的 polyfill,它们将包含在您的包中,以便您可以访问现代功能,例如 async/await。
"name": "2_react-testing-library",
// ...
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react"
]
}
]
]
}
}
在调用 fetch 解析后让您的组件自行更新将导致 App.test.js 中的测试失败。 它会失败,因为它不会在测试完成之前等待 fetch 调用解决。
因为 react-testing-library 在测试完成后卸载组件,所以到 fetch 解析时,组件将不再被挂载,但仍会尝试设置其状态。 这种设置未挂载组件状态的尝试是导致 React 引发错误的原因。
如果组件未挂载,则通过使 App.jsx 避免更新其状态来修复该测试。
import React, { useEffect, useState, useRef } from "react";
// ...
export const App = () => {
const [items, setItems] = useState({});
const isMounted = useRef(null); ❶
useEffect(() => {
isMounted.current = true; ❷
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`)
const responseBody = await response.json();
if (isMounted.current) setItems(responseBody); ❸
};
loadItems();
return () => isMounted.current = false; ❹
}, []);
// ...
};
❶ 创建一个引用,其值将指示是否安装了应用程序
❷ 应用挂载时将 isMounted 设置为 true
❸ 避免在应用不再挂载时更新状态
❹ App卸载时调用的函数,将isMounted设置为false
通过此测试后,您现在必须测试您的应用程序是否会在服务器响应 App 组件发送的请求后显示清单内容。否则,项目列表将始终为空。
要测试 App 是否适当地填充了 ItemList,您应该编写一个测试,该测试能够将 fetch 解析为静态项目列表、呈现 App、等待 App 更新请求的响应,并检查每个列表项目。
对于从服务器获取项目列表的 App 组件,在 App.test.jsx 中添加一个 beforeEach 钩子,它将使用 nock 拦截对 /inventory 的 GET 请求。然后,通过添加清除所有拦截器的 afterEach 钩子,确保每次测试后没有未使用的拦截器。此外,如果 nock.isDone 方法返回 false,这个钩子应该抛出一个错误。
import nock from "nock";
beforeEach(() => { ❶
nock(API_ADDR)
.get("/inventory")
.reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });
});
afterEach(() => { ❷
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
// ...
❶ 在每次测试之前,创建一个拦截器,该拦截器将使用项目列表响应对 /inventory 的 GET 请求
❷ 每次测试后,检查是否已到达所有拦截器,如果未到达,则清除未使用的拦截器并抛出错误
创建这些钩子后,编写一个渲染 App 的测试,等待列表具有三个子项,并检查是否存在预期的列表项。
要等待填充列表,您可以使用 react-testing-library 中的 waitFor 方法。 此方法将重新运行传递给它的函数,直到该函数不抛出任何错误。 因为您的断言在失败时会抛出 AssertionError,所以您可以使用 waitFor 作为它们的重试机制。
import { render, waitFor } from "@testing-library/react";
// ...
test("rendering the server's list of items", async () => {
const { getByText } = render( ); ❶
await waitFor(() => { ❷
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
expect(getByText("cheesecake - Quantity: 2")) ❸
.toBeInTheDocument();
expect(getByText("croissant - Quantity: 5")) ❹
.toBeInTheDocument();
expect(getByText("macaroon - Quantity: 96")) ❺
.toBeInTheDocument();
});
❶ 渲染一个 App 实例
❷ 等待渲染的 ul 元素有三个孩子
❸ 找到一个元素表明库存包含 2 个芝士蛋糕
❹ 找到一个元素,表明库存包含 5 个羊角面包
❺ 找到一个元素,表明库存包含 96 个杏仁饼
在这种情况下,因为您只想等到列表中有项目,所以您将包装到 waitFor 中的唯一断言是检查列表中元素的数量。
如果您还将其他断言包装到 waitFor 中,这些断言可能会因为列表的内容不正确而失败,但是 react-testing-library 会不断重试它们,直到测试超时。
提示 为避免每次需要等待元素时都必须使用 waitFor,您还可以使用 findBy* 而不是 getBy* 查询。
findBy* 查询异步运行。这种查询返回的 promise 要么使用找到的元素解析,要么在一秒钟后拒绝,如果它没有找到与传递的条件匹配的任何内容。
例如,您可以使用它来替换 waitFor,这会导致您的测试等待列表具有三个子项。
您可以做相反的事情,而不是使用 waitFor 函数在运行断言之前等待列表包含三个子项。您可以使用 findByText 等待具有预期文本的元素首先可见,然后才对列表的大小进行断言。
test("rendering the server's list of items", async () => {
const { findByText } = render( ); ❶
expect(await findByText("cheesecake - Quantity: 2")) ❷
.toBeInTheDocument();
expect(await findByText("croissant - Quantity: 5")) ❸
.toBeInTheDocument();
expect(await findByText("macaroon - Quantity: 96")) ❹
.toBeInTheDocument();
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3); ❺
});
❶ 渲染一个 App 实例
❷ 等待一个元素表明库存包含 2 个芝士蛋糕
❸ 等待一个元素表明库存包含 5 个羊角面包
❹ 等待一个元素表明库存包含 96 个杏仁饼
❺ 断言渲染的 ul 元素有三个子元素
始终尝试使您的 waitFor 回调尽可能精简。 否则,它可能会导致您的测试运行时间更长。 正如您在此测试中所做的那样,编写最少数量的断言来验证特定事件是否发生。
注意 在测试 React 应用程序时,我认为组件是原子单元。 因此,与之前的测试不同,我将其归类为集成测试,因为它涉及多个组件。
7.2.3 酶
Enzyme 是一种类似于 react-testing-library 的 React 测试工具。它具有将组件渲染到 DOM、查找元素以及与它们交互的方法。
Enzyme 和 react-testing-library 之间最显着的区别在于它们的仪器方法。 Enzyme 使您可以非常精细地控制组件的内部结构。它允许您以编程方式设置其状态、更新其 props 并访问传递给每个组件子组件的内容。另一方面,react-testing-library 专注于以尽可能少的自省测试组件。它允许您仅像用户那样与组件交互:通过在 DOM 中查找节点并通过它们分派事件。
此外,Enzyme 包含用于执行浅层渲染的实用程序,它仅渲染传递给它的顶级组件。换句话说,Enzyme 的浅渲染不会渲染任何目标的子组件。相比之下,在 react-testing-library 中做到这一点的唯一方法是用 Jest 的测试替身之一手动替换一个组件。
考虑到其广泛而灵活的 API,Enzyme 可以成为一种有吸引力的工具,用于在编写代码时分离编写小型测试并获得快速和细粒度的反馈。使用 Enzyme,可以轻松地将组件彼此隔离,甚至可以在各种测试中隔离组件的不同部分。但是,这种灵活性是以可靠性为代价的,并且会使您的测试套件难以维护且维护成本高昂。
因为 Enzyme 使测试组件的实现细节变得太容易了,所以它可以轻松地将您的测试与组件的实现紧密耦合。这种紧密耦合导致您必须更频繁地更新测试,从而产生更多成本。此外,当浅层渲染组件时,您实际上是用测试替身替换子组件,这会导致您的测试无法代表运行时发生的情况,因此降低了它们的可靠性。
就个人而言,react-testing-library 是我首选的 React 测试工具。我同意这种方法,因为使用较少的测试替身确实使测试更可靠,即使有时我认为该库可以更容易地创建测试替身。此外,它的方法使我能够快速准确地模拟运行时发生的情况,这为我提供了更强的可靠性保证。
注意 在本章的下一节中,我将更详细地解释如何使用测试替身、何时使用以及这样做的利弊。
为简洁起见,我不会详细介绍如何使用 Enzyme,因为在绝大多数情况下,我会推荐 react-testing-library。除了 react-testing-library 的 API 更简洁、更鼓舞人心的模式可以产生更可靠的保证,在撰写本文时,Enzyme 的浅层渲染也无法正确处理许多不同类型的 React hook。因此,如果你想采用 React 钩子,你将无法使用浅层,这是使用 Enzyme 的主要原因之一。
鉴于它仍然是一个流行的工具,并且您可能会在现有项目中找到它,我认为它值得一提。
如果您继续使用 Enzyme,请记住,与将组件渲染到 DOM 以及翻译 JSX 的总体结构相关的相同原则仍然适用。因此,学习如何使用它会相当简单。
注意 如果您有兴趣,可以在 https://enzymejs.github.io/enzyme/ 找到 Enzyme 的文档。
7.2.4 React 测试渲染器
React 自己的测试渲染器是另一个渲染 React 组件的工具。与 Enzyme 或 react-testing-library 不同,它将组件渲染为纯 JavaScript 对象,而不是将它们渲染到 DOM,如图 7.6 所示。
例如,如果您没有使用 JSDOM,或者您不能使用它,它会很有用。因为 React 的测试渲染器不会将您的组件转换为完全成熟的 DOM 节点,所以您不需要任何 DOM 实现来渲染组件并检查其内容。
如果你正在测试一个 Web 应用程序,我看不到使用 React 的测试渲染器的好处,因此,我反对它。设置 JSDOM 相当快,它使您的测试更加可靠,因为它使您的代码像在浏览器中一样运行,更准确地复制您的运行时环境条件。
react-test-renderer 的主要用例是当您不将组件渲染到 DOM 但仍想检查它们的内容时。
例如,如果您有一个 react-native 应用程序,它的组件取决于移动设备的运行时环境。因此,您将无法在 JSDOM 环境中呈现它们。
请记住,React 允许您定义组件以及这些组件的行为方式。在不同平台上实际渲染这些组件的任务是其他工具的责任,您将根据目标环境进行选择。例如,react-dom 包负责将组件渲染到 DOM,这与在移动环境中处理组件的 react-native 不同。
注意你可以在 https://reactjs.org/docs/test-renderer.html 找到 React 测试渲染器的完整文档。
概括
要以类似于它们在浏览器中的工作方式在 Node.js 中测试您的 React 组件,您可以使用 JSDOM。与测试普通 JavaScript 客户端时类似,在测试 React 应用程序时,您可以将组件渲染到 JSDOM 实例,然后使用查询来查找元素,以便您对它们进行断言。
JSX 扩展了 JavaScript 语法,但浏览器或 Node.js 无法理解它。就像您必须将 JSX 代码编译为纯 JavaScript 才能在浏览器中运行它一样,您需要配置 Jest 以将 JSX 转换为纯 JavaScript,然后才能运行测试。
每当使用无法在您使用的 Node.js 版本中运行的代码时,您都需要将其编译为纯支持的 JavaScript,然后才能使用 Jest 运行测试。例如,如果您使用 ES 导入,您可能需要转换文件。
要测试您的 React 应用程序,您可以使用 react-testing-library,其 API 类似于您在前一章中看到的 dom-testing-library 包。这两个库之间的区别在于 react-testing-library 开箱即用地解决了特定于 React 的问题。这些问题包括自动卸载组件、返回作用域为组件包装器的查询,以及将交互包装成行为以确保更新被处理并应用于 DOM。
要在 React 应用程序的测试中处理 HTTP 请求,您可以像测试普通 JavaScript 应用程序时一样使用 nock。如果您需要在请求解决时等待组件更新,或者当外部数据源为其提供数据时,您可以使用 react-testing-library 中的 waitFor 函数。使用 waitFor,您可以重试断言,直到它成功,然后才继续执行其他操作或验证。
酶是 react-testing-library 的流行替代品。与 react-testing-library 不同,Enzyme 允许您直接与组件的内部方面进行交互,例如它们的 props 和 state。此外,它的浅渲染功能可以更轻松地隔离测试。因为这些特性使您的测试不同于运行时发生的情况,所以它们以可靠性为代价。
如果您的 React 应用程序将组件渲染到 DOM 以外的目标,就像 React Native 那样,您可以使用 React 的测试渲染器将组件渲染到纯 JavaScript 对象。
特别是在测试 React 应用程序时,将测试金字塔视为一个连续的频谱,而不是一组离散的类别是很有趣的。由于 React 和您的测试之间的集成层非常薄,我会将涉及单个组件的测试放在金字塔的底部,即使它们不存根 React 本身。测试涉及的组件和不同的代码段越多,它在金字塔中的位置就越高。