作者选择了 Vets Who Code 作为 Write for DOnations 计划的捐赠对象。
介绍
获得可靠的测试覆盖率对于建立对您的 Web 应用程序的信心至关重要。 Jest 是一个 JavaScript 测试运行器,提供了编写和运行测试的资源。 React Testing Library 提供了一组测试助手,根据用户交互而不是组件的实现细节来组织您的测试。 Jest 和 React Testing Library 都预装在 Create React App 中,并遵循测试应用程序应该类似于软件将如何使用的指导原则。
在本教程中,您将在包含各种 UI 元素的示例项目中测试异步代码和交互。 您将使用 Jest 编写和运行单元测试,并将 React Testing Library 实现为一个辅助 DOM(文档对象模型)库来处理与组件的交互。
先决条件
完成本教程需要以下内容:
-
Node.js 版本14或更高版本已安装在您的本地计算机上。要在macOS或Ubuntu 18.04上安装Node.js,请按照如何在macOS上安装Node.js并创建本地开发环境中的步骤,或者在使用PPA安装部分查看如何在Ubuntu 18.04上安装Node.js。
-
在您的本地计算机上安装
npm
版本为 5.2 或更高版本,您将需要在示例项目中使用 Create React App 和npx
。如果您还没有安装npm
,请立即安装。对于 Linux,请使用命令sudo apt install npm
。- 为了使本教程中的
npm
包正常工作,请安装build-essential
包。对于 Linux,请使用命令sudo apt install build-essential
。
- 为了使本教程中的
-
在您的本地计算机上安装 Git。您可以检查 Git 是否已安装在您的计算机上,或者按照您的操作系统进行安装过程,参考 在 Ubuntu 20.04 上安装 Git。
-
熟悉React,您可以通过如何在React.js中编码系列进行开发。因为示例项目是使用Create React App引导的,所以您无需单独安装。
-
对于作为测试运行器或框架的
有一定了解是有帮助的,但并非必需。因为Jest已经与Create React App预先打包,所以您无需单独安装。
步骤1 — 设置项目
在这一步中,您将克隆一个示例项目并启动测试套件。示例项目利用了三个主要工具:Create React App、Jest 和 React Testing Library。Create React App 用于引导单页 React 应用程序。Jest 用作测试运行器,而 React Testing Library 则提供了围绕用户交互构建测试的辅助工具。
要开始,您将从 GitHub 克隆一个预先构建的 React 应用程序。您将使用Doggy Directory应用程序,这是一个利用Dog API构建的示例项目,用于基于特定品种的狗图像集合构建搜索和显示系统。
要从 Github 克隆项目,请打开您的终端并运行以下命令:
您将看到类似于此的输出:
OutputCloning into 'doggy-directory'...
remote: Enumerating objects: 64, done.
remote: Counting objects: 100% (64/64), done.
remote: Compressing objects: 100% (48/48), done.
remote: Total 64 (delta 21), reused 55 (delta 15), pack-reused 0
Unpacking objects: 100% (64/64), 228.16 KiB | 3.51 MiB/s, done.
切换到doggy-directory
文件夹:
安装项目依赖项:
npm install
命令将安装package.json
文件中定义的所有项目依赖项。
安装完依赖项后,您可以查看应用程序的部署版本,或者您可以使用以下命令在本地运行应用程序:
如果选择在本地运行应用程序,则会在http://localhost:3000/
上打开。您将在终端中看到以下输出:
OutputCompiled successfully!
You can now view doggy-directory in the browser.
Local: http://localhost:3000
On Your Network: http://network_address:3000
启动后,应用程序的登录页面将如下所示:
项目依赖已安装,并且应用现在正在运行。接下来,打开一个新的终端并使用以下命令启动测试:
使用 npm test
命令以交互式监视模式启动测试,Jest 作为测试运行程序。在监视模式下,测试会在文件更改后自动重新运行。测试会在你更改文件时运行,并告诉你该更改是否通过了测试。
第一次运行 npm test
后,你会在终端看到以下输出:
OutputNo tests found related to files changed since last commit.
Press `a` to run all tests, or run Jest with `--watchAll`.
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press q to quit watch mode.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press Enter to trigger a test run.
现在你已经运行了示例应用程序和测试套件,你可以开始测试登陆页面。
第二步 — 测试登陆页面
默认情况下,Jest 会查找具有 .test.js
后缀和具有 .js
后缀的文件在 __tests__
文件夹中。当你对相关的测试文件进行更改时,它们将被自动检测到。随着测试用例的修改,输出将自动更新。为 doggy-directory
示例项目准备的测试文件在添加测试范例之前设置了最小的代码。在这一步中,你将编写测试来验证应用程序的登陆页面在执行搜索之前能够加载。
在你的编辑器中打开 src/App.test.js
,你会看到以下代码:
A minimum of one test block is required in each test file. Each test block accepts two required parameters: the first argument is a string representing the name of the test case; the second argument is a function that holds the expectations of the test.
在函数内部,React Testing Library 提供了一个render
方法,用于将您的组件渲染到 DOM 中。将要测试的组件渲染到测试环境的 DOM 中后,您现在可以开始编写代码来断言预期功能。
您将在render
方法中添加一个测试块,以测试在进行任何 API 调用或选择之前着陆页面是否准确渲染。在render
方法下方添加以下突出显示的代码:
expect
函数每次您想要验证特定结果时都会使用,它接受一个参数,表示您的代码产生的值。大多数expect
函数都与一个匹配器函数配对,以断言关于特定值的某些内容。对于大多数这些断言,您将使用由jest-dom提供的额外匹配器,以便更轻松地检查在 DOM 中发现的常见方面。例如,.toHaveTextContent
是第一行中expect
函数的匹配器,而getByRole("heading")
是用于获取 DOM 元素的选择器。
React Testing Library 提供了screen
对象,作为访问断言测试 DOM 环境所需的相关查询的便捷方式。默认情况下,React Testing Library 提供了允许您在 DOM 中定位元素的查询。查询有三个主要类别:
getBy*
(最常用)queryBy*
(在测试时用于检测元素不存在而不抛出错误时使用)findBy*
(在测试异步代码时使用)
每种查询类型都有特定的目的,稍后在教程中将对其进行定义。在这一步中,您将专注于getBy*
查询,这是最常见的查询类型。要查看不同查询变体的详尽列表,您可以查阅React的查询速查表。
下面是Doggy Directory登录页面的注释图像,指示第一个测试(渲染登录页面)覆盖的每个部分:
每个expect
函数都是根据以下内容(如上述注释图像中所示)进行断言:
- 您期望具有标题角色的元素具有子字符串匹配Doggy Directory。
- 您期望选择输入具有选择品种的确切显示值。
- 您期望搜索按钮处于禁用状态,因为尚未进行选择。
- 您期望占位图像存在于文档中,因为尚未进行搜索。
完成后,保存src/App.test.js
文件。因为测试正在以监视模式运行,更改将自动注册。如果更改没有自动注册,您可能需要停止并重新启动测试套件。
现在,当您在终端中查看测试时,您将看到以下输出:
Output PASS src/App.test.js
✓ renders the landing page (172 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.595 s, estimated 5 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
在此步骤中,您编写了一个初始测试,以验证Doggy Directory登录页面的初始渲染视图。在下一步中,您将学习如何模拟API调用以测试异步代码。
第3步 – 模拟fetch
方法
在此步骤中,您将回顾一种模拟JavaScript的fetch
方法的方法。虽然有多种方法可以实现这一点,但此实现将使用Jest的spyOn
和mockImplementation
方法。
当您依赖外部API时,它们的API可能会崩溃或花费一段时间才能返回响应。模拟fetch
方法提供了一个一致和可预测的环境,使您对测试更有信心。API模拟机制是必要的,以正确运行使用外部API的测试。
注意:为了简化这个项目,你将模拟 fetch 方法。但是,建议你在模拟异步代码时使用更健壮的解决方案,比如Mock Service Worker(MSW),适用于更大规模、适合生产环境的代码库。
打开编辑器中的 src/mocks/mockFetch.js
,查看 mockFetch
方法的工作原理:
mockFetch
方法返回一个对象,其结构与应用程序中 API 调用的 fetch
调用返回的结构非常相似。mockFetch
方法对测试狗狗目录应用程序中两个区域的异步功能至关重要:填充品种列表的选择下拉菜单以及执行搜索时检索狗狗图片的 API 调用。
关闭 src/mocks/mockFetch.js
。现在你了解了 mockFetch
方法将如何在你的测试中使用,你可以将其导入到测试文件中。将 mockFetch
函数作为参数传递给 mockImplementation
方法,然后将其用作 fetch API 的仿真实现。
在 src/App.test.js
中,添加以下代码中的突出显示的行以导入 mockFetch
方法:
此代码将设置并拆除模拟实现,以便每个测试都从一个公平的起点开始。
jest.spyOn(window, "fetch");
创建一个模拟函数,用于跟踪附加到全局 window 变量的 DOM 中的 fetch
方法的调用。
.mockImplementation(mockFetch);
接受一个函数,该函数将被用于实现模拟方法。因为这个命令覆盖了原始的 fetch
实现,所以每当在应用代码中调用 fetch
时,它都会运行。
完成后,保存 src/App.test.js
文件。
现在,当您在终端中查看测试时,您将收到以下输出:
Output console.error
Warning: An update to App inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at App (/home/sammy/doggy-directory/src/App.js:5:31)
18 | })
19 | .then((json) => {
> 20 | setBreeds(Object.keys(json.message));
| ^
21 | });
22 | }, []);
23 |
...
PASS src/App.test.js
✓ renders the landing page (429 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.178 s, estimated 2 s
Ran all test suites related to changed files.
警告告诉您发生了状态更新,但输出还表明测试已成功模拟了 fetch
方法。
在这一步中,您模拟了 fetch
方法并将该方法合并到了一个测试套件中。虽然测试通过了,但您仍然需要处理这个警告。
第 4 步 —— 修复 act
警告
在这一步中,您将学习如何修复第 3 步更改后出现的 act
警告。
act
警告是因为您已经模拟了 fetch
方法,当组件挂载时,它会发出一个 API 调用来获取品种列表。品种列表存储在一个状态变量中,该变量填充了选择输入中的 option
元素。
下面的图片显示了在成功进行 API 调用以填充品种列表后选择输入的样子:
警告是因为状态在测试块完成渲染组件后被设置。
为了解决这个问题,在src/App.test.js
中添加如下突出显示的修改:
async
关键字告诉Jest,异步代码是由组件挂载时发生的API调用导致的。
A new assertion with the findBy
query verifies that the document contains an option with the value of husky
. findBy
queries are used when you need to test asynchronous code that is dependent on something being in the DOM after a period of time. Because the findBy
query returns a promise that gets resolved when the requested element is found in the DOM, the await
keyword is used within the expect
method.
完成后,请保存对src/App.test.js
的更改。
有了这些新添加的内容,您将会发现在测试中不再出现act
警告:
Output PASS src/App.test.js
✓ renders the landing page (123 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.942 s, estimated 2 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
在这一步骤中,您学会了如何修复在处理异步代码时可能出现的act
警告。接下来,您将添加第二个测试用例来验证Doggy Directory应用程序的交互功能。
第5步 — 测试搜索功能
在最后一步,您将编写一个新的测试用例来验证搜索和图像显示功能。您将利用各种查询和API方法来实现正确的测试覆盖率。
返回编辑器中的src/App.test.js
文件。在文件顶部,使用以下突出显示的命令导入user-event
伴侣库和waitForElementToBeRemoved
异步方法到测试文件中:
您将在本节稍后使用这些导入。
在初始test()
方法之后,添加一个新的异步测试块,并使用以下代码块呈现App
组件:
组件呈现后,您现在可以添加验证Doggy Directory应用程序的交互功能的函数。
仍然在src/App.test.js
中,在第二个test()
方法中添加突出显示的代码块:
上面突出显示的部分将模拟选择狗品种并验证正确的值是否显示。
getByRole
查询抓取所选元素并将其分配给select
变量。
类似于您如何在第4步中修复act
警告,使用findByRole
查询等待cattledog
选项出现在文档中,然后进行进一步的断言。
前面导入的userEvent
对象将模拟常见的用户交互。在这个示例中,selectOptions
方法选择了你在上一行等待的cattledog
选项。
最后一行断言select
变量包含上面选择的cattledog
值。
你将添加到Javascript test()
块的下一部分将发起搜索请求,以根据所选择的品种找到狗的图像,并确认加载状态的存在。
添加以下突出显示的行:
getByRole
查询定位搜索按钮,并将其分配给searchBtn
变量。
toBeDisabled
jest-dom匹配器将验证在进行品种选择时搜索按钮未被禁用。
click
方法在userEvent
对象上模拟点击搜索按钮。
waitForElementToBeRemoved
异步帮助函数 之前导入的会在搜索 API 调用进行时等待 Loading 消息的出现和消失。 waitForElementToBeRemoved
回调内的 queryByText
检查元素是否不存在而不会抛出错误。
下面的图片显示了搜索进行中时将显示的加载状态:
接下来,添加以下 JavaScript 代码来验证图片和结果计数的显示:
getAllByRole
查询将选择所有狗的图片并将它们分配给 dogImages
变量。 查询的 *AllBy*
变体返回一个包含多个与指定角色匹配的元素的数组。 *AllBy*
变体不同于 ByRole
变体,后者只能返回单个元素。
模拟的 fetch
实现在响应中包含了两个图片 URL。使用 Jest 的 toHaveLength
匹配器,您可以验证显示了两张图片。
getByText
查询将检查右上角出现的正确结果计数。
使用 toHaveAccessibleName
匹配器的两个断言将验证与单个图像关联的适当 alt 文本。
A completed search displaying images of the dog based on the breed selected along with the number of results found will look like this:
当您将新的 JavaScript 代码的所有部分组合在一起时,App.test.js
文件将如下所示:
保存在 src/App.test.js
中所做的更改。
当您审查您的测试时,终端中的最终输出现在将具有以下输出:
Output PASS src/App.test.js
✓ renders the landing page (273 ms)
✓ should be able to search and display dog image results (123 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.916 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
在这最后一步中,您添加了一个测试,以验证 Doggy Directory 应用程序的搜索、加载和显示功能。随着最终断言的编写,您现在知道您的应用程序可以正常工作。
结论
在本教程中,您使用 Jest、React 测试库和 jest-dom 匹配器编写了测试用例。逐步构建,您根据用户与 UI 的交互方式编写了测试。您还学习了 getBy*、findBy* 和 queryBy* 查询之间的区别,以及如何测试异步代码。
要了解上述提到的主题的更多信息,请查看Jest、React Testing Library和jest-dom的官方文档。您还可以阅读 Kent C. Dodd 的使用 React Testing Library 时的常见错误,以了解在使用 React Testing Library 时的最佳实践。有关在 React 应用程序中使用快照测试的更多信息,请查看如何编写快照测试。