如何使用 Jest 和 React 测试库测试 React 应用程序

作者选择了 Vets Who Code以获得捐赠作为 Write for Donations计划的一部分。

介绍

获得坚实的测试覆盖是建立对您的 Web 应用程序的信心至关重要的。 Jest是一个为编写和运行测试提供资源的 JavaScript 测试运行器。 React Testing Library提供了基于用户交互而不是组件的实施细节来构建测试辅助器的集合。

在本教程中,您将测试包含各种用户界面元素的样本项目中的非同步代码和交互,您将使用Jest来编写和运行单元测试,您将实施React Testing Library作为一个辅助DOM(Document Object Model)库来处理与组件的交互。

前提条件

要完成本教程,您将需要:

  • Node.js版本14或更多安装在您的本地机上. 要在 macOS 或 Ubuntu 18.04 上安装 Node.js, 请遵循 [如何在 macOS 上安装 Node.js 并创建本地开发环境 (https://andsky.com/tech/tutorials/how-to-install-node-js-and-create-a-local-development-environment-on-macos) 或 [如何在 Ubuntu 18.04 上安装 Node.js (https://andsky.com/tech/tutorials/how-to-install-node-js-on-ubuntu-18-04 ) 中的步骤。
  • 本地机上的 " npm " 版本为5.2或更多,在样本项目中需要使用Create React App和 " npx " 。 如果你不在Node.js'旁边安装npm',现在就这样做。 对于 Linux ,使用 sudo apt s安装 npm 命令。
  • npm 包在此教程中工作,安装建设-必需包。 对于Linux,使用sudo apt setting-built-gency命令。

步骤1 - 创建项目

在此步骤中,您将克隆样本项目并启动测试套件。样本项目使用三个主要工具:Create React App、Jest 和 React Testing Library。Create React App 用于启动单页的 React 应用程序。Jest 被用作测试运行器,React Testing Library 提供测试助手来构建围绕用户交互的测试。

首先,您将从 GitHub 克隆预先构建的 React 应用程序,您将与 Doggy Directory应用程序合作,这是一个采样项目,利用 Dog API构建一个基于特定品种的狗图像集的搜索和显示系统。

要从 Github 克隆项目,请打开终端并执行以下命令:

1git clone https://github.com/do-community/doggy-directory

您将看到类似于此的输出:

1[secondary_label Output]
2Cloning into 'doggy-directory'...
3remote: Enumerating objects: 64, done.
4remote: Counting objects: 100% (64/64), done.
5remote: Compressing objects: 100% (48/48), done.
6remote: Total 64 (delta 21), reused 55 (delta 15), pack-reused 0
7Unpacking objects: 100% (64/64), 228.16 KiB | 3.51 MiB/s, done.

更改到「doggy-directory」文件夹:

1cd doggy-directory

安装项目依赖性:

1npm install

npm install命令将安装在package.json文件中定义的所有项目依赖。

安装依赖程序后,您可以查看 应用程序的部署版本或使用以下命令本地运行应用程序:

1npm start

如果您选择在本地运行该应用程序,它将打开在http://localhost:3000/

1[secondary_label Output]
2Compiled successfully!
3
4You can now view doggy-directory in the browser.
5
6Local:            http://localhost:3000
7On Your Network:  http://network_address:3000

启动后,应用程序的登陆页面将看起来像这样:

Landing page

项目依赖已安装,应用程序现在正在运行. 接下来,打开一个新的终端,并使用以下命令启动测试:

1npm test

npm test 命令将以 Jest 作为测试运行器在交互式观看模式中启动测试。 在观看模式下,测试在文件被更改后自动重新运行。

在首次运行npm 测试后,您将在终端中看到此输出:

 1[secondary_label Output]
 2No tests found related to files changed since last commit.
 3Press `a` to run all tests, or run Jest with `--watchAll`.
 4
 5Watch Usage
 6  Press a to run all tests.
 7  Press f to run only failed tests.
 8  Press q to quit watch mode.
 9  Press p to filter by a filename regex pattern.
10  Press t to filter by a test name regex pattern.
11  Press Enter to trigger a test run.

现在您已经运行了示例应用程序和测试套件,您可以开始使用定位页进行测试。

步骤二:测试登陆页

默认情况下,Jest 会在__tests__文件夹中搜索带有.test.js字符串的文件和带有.js字符串的文件。当您对相关的测试文件进行更改时,它们将自动检测到。随着测试案例的修改,输出将自动更新。为doggy-directory样本项目准备的测试文件在添加测试范式之前设置为最小代码。

在您的编辑器中打开src/App.test.js以查看以下代码:

1[label src/App.test.js]
2import { render, screen } from '@testing-library/react';
3import App from './App';
4
5test('renders the landing page', () => {
6  render(<App />);
7});

在每个测试文件中至少需要一个 test block 每个测试块接受两个所需参数:第一个参数是表示测试案例名称的字符串;第二个参数是包含测试预期的函数。

在函数中,有一个 render 方法,React Testing Library 提供将您的组件渲染到 DOM. 随着您想要测试的组件渲染到测试环境的 DOM,您现在可以开始写代码到 assert 对预期的功能。

您将向渲染方法添加一个测试块,该方法将在任何 API 调用或选择之前测试目标页是否准确渲染。

 1[label src/App.test.js]
 2...
 3test('renders the landing page', () => {
 4  render(<App />);
 5
 6  expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/);
 7  expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed");
 8  expect(screen.getByRole("button", { name: "Search" })).toBeDisabled();
 9  expect(screen.getByRole("img")).toBeInTheDocument();
10});

expect函数每当你想要验证某个结果时都使用,它接受一个代表你的代码产生的值的单个参数。大多数‘expect’函数与一个 matcher 函数配对,以声称某个特定值。对于大多数这些陈述,你会使用 jest-dom提供的额外匹配,以便更容易查找 DOM 中的常见方面。

React Testing Library 提供屏幕对象,以便方便地访问对测试 DOM 环境进行辩护所需的相关查询. 默认情况下,React Testing Library 提供了查询,允许您在 DOM 中找到元素。

  • getBy*(最常用的)
  • queryBy*(在测试元素的缺失时使用,而不扔错误)
  • findBy*(在测试非同步代码时使用)

每个查询类型都有一个特定的目的,将在教程中稍后定义。在此步骤中,您将专注于getBy*查询,这是最常见的查询类型。

下面是 Doggy Directory 定位页的注释图像,显示了第一个测试(在渲染定位页时)所涵盖的每个部分:

Landing page annotated

每个期望函数都反对以下(如上图所示):

您期望具有 标题角色的元素具有 Doggy Directorysubstring 匹配性。 2 您期望所选择的输入具有 选择品种 的准确显示值。 3 您期望 搜索 按钮被禁用,因为没有进行选择。

完成後,保存「src/App.test.js」檔案. 因為測試在 watch 模式下執行,變更會自動註冊。

现在,当您查看终端中的测试时,您将看到以下输出:

 1[secondary_label Output]
 2 PASS src/App.test.js
 3  ✓ renders the landing page (172 ms)
 4
 5Test Suites: 1 passed, 1 total
 6Tests:       1 passed, 1 total
 7Snapshots:   0 total
 8Time:        2.595 s, estimated 5 s
 9Ran all test suites related to changed files.
10
11Watch Usage: Press w to show more.

在此步骤中,您写了一篇初步测试,以验证 Doggy Directory 定位页的初始渲染视图. 在下一步中,您将学习如何嘲笑 API 调用来测试非同步代码。

步骤3 – 嘲笑fetch方法

在此步骤中,您将审查一个方法来嘲笑JavaScript的 fetch方法.虽然有许多方法来实现这一点,但这个实现将使用Jest的 spyOnmockImplementation方法。

当您依赖于外部 API 时,有可能他们的 API 会下降或需要一段时间才能返回回响应。fetch 方法的嘲笑提供了一致和可预测的环境,为您的测试提供了更多的信心。

<$>[注] 注: 为了让这个项目变得简单,你会嘲笑收集方法,但是,在嘲笑更大的、生产准备的代码库时,建议你使用更强大的解决方案,例如 Mock Service Worker(MSW)。

在编辑器中打开src/mocks/mockFetch.js,查看mockFetch方法的工作方式:

 1[label src/mocks/mockFetch.js]
 2const breedsListResponse = {
 3    message: {
 4        boxer: [],
 5        cattledog: [],
 6        dalmatian: [],
 7        husky: [],
 8    },
 9};
10
11const dogImagesResponse = {
12    message: [
13        "https://images.dog.ceo/breeds/cattledog-australian/IMG_1042.jpg ",
14        "https://images.dog.ceo/breeds/cattledog-australian/IMG_5177.jpg",
15    ],
16};
17
18export default async function mockFetch(url) {
19    switch (url) {
20        case "https://dog.ceo/api/breeds/list/all": {
21            return {
22                ok: true,
23                status: 200,
24                json: async () => breedsListResponse,
25            };
26        }
27        case "https://dog.ceo/api/breed/husky/images" :
28        case "https://dog.ceo/api/breed/cattledog/images": {
29            return {
30                ok: true,
31                status: 200,
32                json: async () => dogImagesResponse,
33            };
34        }
35        default: {
36            throw new Error(`Unhandled request: ${url}`);
37        }
38    }
39}

mockFetch方法返回一个类似于fetch呼叫响应应用程序中的API呼叫的结构的对象。

关闭src/mocks/mockFetch.js。现在你已经了解mockFetch方法将如何在测试中使用,你可以将其导入到测试文件中。

src/App.test.js中,添加突出的代码行以导入mockFetch方法:

 1[label src/App.test.js]
 2import { render, screen } from '@testing-library/react';
 3import mockFetch from "./mocks/mockFetch";
 4import App from './App';
 5
 6beforeEach(() => {
 7   jest.spyOn(window, "fetch").mockImplementation(mockFetch);
 8})
 9
10afterEach(() => {
11   jest.restoreAllMocks()
12});
13...

此代码将设置并破坏模仿实现,以便每个测试从平等的比赛场所开始。

jest.spyOn(窗口,fetch); 创建了一个模仿函数,该函数将跟踪在 DOM 中连接到全球 窗口变量的 `fetch’ 方法的呼叫。

.mockImplementation(mockFetch); 接受将用于实现模仿方法的函数. 由于此命令超越了原始的 fetch 实现,因此每次在应用程序代码中调用 fetch 时,它就会运行。

完成后,保存src/App.test.js文件。

现在,当您在终端查看测试时,您将收到以下输出:

 1[secondary_label Output]
 2  console.error
 3    Warning: An update to App inside a test was not wrapped in act(...).
 4
 5    When testing, code that causes React state updates should be wrapped into act(...):
 6
 7    act(() => {
 8      /* fire events that update state */
 9    });
10    /* assert on the output */
11
12    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
13        at App (/home/sammy/doggy-directory/src/App.js:5:31)
14
15      18 |       })
16      19 |       .then((json) => {
17    > 20 |         setBreeds(Object.keys(json.message));
18         |         ^
19      21 |       });
20      22 |   }, []);
21      23 |
22 ...
23
24 PASS src/App.test.js
25  ✓ renders the landing page (429 ms)
26
27Test Suites: 1 passed, 1 total
28Tests:       1 passed, 1 total
29Snapshots:   0 total
30Time:        1.178 s, estimated 2 s
31Ran all test suites related to changed files.

警告告您,状态更新发生在没有预期时,但输出也表明测试成功模拟了fetch方法。

在此步骤中,您嘲笑了fetch方法,并将该方法纳入测试套件中,尽管测试正在通过,但您仍然需要解决警告。

步骤 4 – 修复行为警告

在此步骤中,您将学习如何修复在步骤 3 中的更改后出现的行为警告。

行为警告发生,因为你嘲笑了fetch方法,当组件安装时,它会发出API呼叫来抓住品种列表。

下面的图像显示了选择的输入在成功的API调用后如何填充品种列表:

Select populated

警告被扔掉,因为测试块完成渲染组件后设置状态。

若要修复此问题,请在src/App.test.js中添加测试案例中的突出更改:

 1[label src/App.test.js]
 2...
 3test('renders the landing page', async () => {
 4   render(<App />);
 5
 6   expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/);
 7   expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed");
 8   expect(await screen.findByRole("option", { name: "husky"})).toBeInTheDocument();
 9   expect(screen.getByRole("button", { name: "Search" })).toBeDisabled();
10   expect(screen.getByRole("img")).toBeInTheDocument();
11});

关键字 async 告诉 Jest 非同步代码是由于组件安装时发生的 API 调用而运行。

使用 [findBy] 查询(https://testing-library.com/docs/dom-testing-library/api-async/#findby-queries)的新声明验证文档中包含一个具有 husky 值的选项。 findBy 查询用于当您需要在一段时间后测试依赖于 DOM 中的某些东西的非同步代码。

完成后,将所做的更改保存到「src/App.test.js」。

随着新的添加,您现在将看到行为警告不再存在于您的测试中:

 1[secondary_label Output]
 2 PASS src/App.test.js
 3  ✓ renders the landing page (123 ms)
 4
 5Test Suites: 1 passed, 1 total
 6Tests:       1 passed, 1 total
 7Snapshots:   0 total
 8Time:        0.942 s, estimated 2 s
 9Ran all test suites related to changed files.
10
11Watch Usage: Press w to show more.

在此步骤中,您了解了如何修复在使用非同步代码时可能发生的行为警告,接下来,您将添加第二个测试案例来验证Doggy Directory应用程序的交互功能。

步骤5:测试搜索功能

在最后一步中,您将写一个新的测试案例来验证搜索和图像显示功能,您将利用各种查询和API方法实现适当的测试覆盖。

在文件的顶部,导入 user-event的伴侣库和 waitForElementToBeRemoved async 方法到测试文件中,以突出命令:

1[label src/App.test.js]
2import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';import userEvent from '@testing-library/user-event'; 
3...

您将在本节中稍后使用这些导入。

在最初的 test() 方法之后,添加一个新的 async 测试块,并以以下代码块渲染 App 组件:

1[label src/App.test.js]
2...
3test("should be able to search and display dog image results", async () => {
4   render(<App />);
5})

随着元件的渲染,您现在可以添加功能来验证Doggy Directory应用程序的交互功能。

src/App.test.js中,在第二个test()方法中添加突出的代码块:

 1[label src/App.test.js]
 2...
 3test("should be able to search and display dog image results", async () => {
 4   render(<App />);
 5
 6   //Simulate selecting an option and verifying its value
 7   const select = screen.getByRole("combobox");
 8   expect(await screen.findByRole("option", { name: "cattledog"})).toBeInTheDocument();
 9   userEvent.selectOptions(select, "cattledog");
10   expect(select).toHaveValue("cattledog");
11})

上面突出的部分将模拟狗品种的选择,并验证显示正确的值。

getByRole查询捕捉了所选元素并将其分配给选择变量。

与您在步骤 4 中修复行为警告的方式类似,使用findByRole查询等待cattledog选项在文档中出现,然后再进行进一步的声明。

早些时候导入的userEvent对象会模拟常见的用户交互,在此示例中,selectOptions方法(https://testing-library.com/docs/ecosystem-user-event/#selectoptionselement-values-options)会选择您在上一行中等待的cattledog选项。

最后一行声称选择变量包含上述选择的cattledog值。

您将添加到JavaScripttest()块的下一个部分将启动搜索请求,以根据所选品种找到狗的图像,并确认加载状态的存在。

添加突出的线条:

 1[label src/App.test.js]
 2...
 3test("should be able to search and display dog image results", async () => {
 4   render(<App />);
 5
 6   //...Simulate selecting an option and verifying its value
 7
 8  //Simulate initiating the search request
 9   const searchBtn = screen.getByRole("button", { name: "Search" });
10   expect(searchBtn).not.toBeDisabled();
11   userEvent.click(searchBtn);
12
13   //Loading state displays and gets removed once results are displayed
14   await waitForElementToBeRemoved(() => screen.queryByText(/Loading/i));
15})

getByRole查询会找到搜索按钮,并将其分配给searchBtn变量。

toBeDisabled) jest-dom 匹配器将在进行品种选择时检查搜索按钮是否禁用。

userEvent对象上的 click方法模拟了点击搜索按钮。

之前导入的WaitForElementToBeRemoved async helper 函数将在搜索 API 调用时等待 Loading 消息的出现和消失。

下面的图像显示在进行搜索时将显示的加载状态:

Loading view

接下来,添加以下JavaScript代码来验证图像和结果计数显示:

 1[label src/App.test.js]
 2...
 3test("should be able to search and display dog image results", async () => {
 4   render(<App />)
 5
 6   //...Simulate selecting an option and verifying its value
 7   //...Simulate initiating the search request
 8   //...Loading state displays and gets removed once results are displayed
 9
10   //Verify image display and results count
11   const dogImages = screen.getAllByRole("img");
12   expect(dogImages).toHaveLength(2);
13   expect(screen.getByText(/2 Results/i)).toBeInTheDocument();
14   expect(dogImages[0]).toHaveAccessibleName("cattledog 1 of 2");
15   expect(dogImages[1]).toHaveAccessibleName("cattledog 2 of 2");
16})

getAllByRole查询将选择所有狗图像,并将它们分配到dogImages变量中。 查询的*AllBy*变量返回包含与指定的角色相匹配的多个元素的数组。

被嘲笑的fetch实现包含了响应中的两个图像 URL. 使用 Jest 的 toHaveLength匹配器,您可以验证显示了两个图像。

getByText查询将检查正确的结果计数是否出现在右角。

使用 toHaveAccessibleName匹配符的两个声明验证了相应的alt 文本与单个图像相关联。

根据所选择的品种和找到的结果的数量显示狗的图像的完成搜索将看起来如下:

Image results display

当你将新JavaScript代码的所有部分组合在一起时,App.test.js文件将看起来像这样:

 1[label src/App.test.js]
 2import {render, screen, waitForElementToBeRemoved} from '@testing-library/react';
 3import userEvent from '@testing-library/user-event';
 4import mockFetch from "./mocks/mockFetch";
 5import App from './App';
 6
 7beforeEach(() => {
 8   jest.spyOn(window, "fetch").mockImplementation(mockFetch);
 9})
10
11afterEach(() => {
12   jest.restoreAllMocks();
13});
14
15test('renders the landing page', async () => {
16   render(<App />);
17
18   expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/);
19   expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed");
20   expect(await screen.findByRole("option", { name: "husky"})).toBeInTheDocument()
21   expect(screen.getByRole("button", { name: "Search" })).toBeDisabled();
22   expect(screen.getByRole("img")).toBeInTheDocument();
23});
24
25test("should be able to search and display dog image results", async () => {
26   render(<App />);
27
28   //Simulate selecting an option and verifying its value
29   const select = screen.getByRole("combobox");
30   expect(await screen.findByRole("option", { name: "cattledog"})).toBeInTheDocument();
31   userEvent.selectOptions(select, "cattledog");
32   expect(select).toHaveValue("cattledog");
33
34   //Initiate the search request
35   const searchBtn = screen.getByRole("button", { name: "Search" });
36   expect(searchBtn).not.toBeDisabled();
37   userEvent.click(searchBtn);
38
39   //Loading state displays and gets removed once results are displayed
40   await waitForElementToBeRemoved(() => screen.queryByText(/Loading/i));
41
42   //Verify image display and results count
43   const dogImages = screen.getAllByRole("img");
44   expect(dogImages).toHaveLength(2);
45   expect(screen.getByText(/2 Results/i)).toBeInTheDocument();
46   expect(dogImages[0]).toHaveAccessibleName("cattledog 1 of 2");
47   expect(dogImages[1]).toHaveAccessibleName("cattledog 2 of 2");
48})

在「src/App.test.js」中保存所做的更改。

当您审查测试时,终端的最终输出现在将有以下输出:

 1[secondary_label Output]
 2 PASS src/App.test.js
 3  ✓ renders the landing page (273 ms)
 4  ✓ should be able to search and display dog image results (123 ms)
 5
 6Test Suites: 1 passed, 1 total
 7Tests:       2 passed, 2 total
 8Snapshots:   0 total
 9Time:        4.916 s
10Ran all test suites related to changed files.
11
12Watch Usage: Press w to show more.

在此最后一步中,您添加了一项测试,该测试验证了Doggy Directory应用程序的搜索、加载和显示功能。

结论

在本教程中,您使用Jest,React Testing Library和Jest-dom匹配器撰写了测试案例. 逐步构建,您根据用户如何与UI互动撰写了测试。

若要了解有关上述主题的更多信息,请参阅 Jest, React Testing Library,以及 jest-dom的官方文档。 您还可以阅读 Kent C. Dodd 的 React Testing Library Common Mistakes 以了解与 React Testing Library 合作的最佳做法。 有关使用 React App 中的快照测试的更多信息,请参阅 How To Write Snapshot Tests

Published At
Categories with 技术
comments powered by Disqus