通过 React 集成测试提高代码可维护性

已发表: 2022-03-11

集成测试是测试成本和价值之间的最佳平衡点。 在 react-testing-library 的帮助下为 React 应用程序编写集成测试,而不是组件单元测试,可以在不影响开发速度的情况下提高代码的可维护性。

如果您想在我们继续之前抢先一步,您可以在此处查看如何使用 react-testing-library 进行 React 应用程序集成测试的示例。

为什么投资集成测试?

“集成测试在信心和速度/费用之间的权衡上取得了很好的平衡。这就是为什么建议将大部分精力(不是全部,请注意)花在上面的原因。”
– Kent C. Dodds 在写测试中。 不是很多。 主要是集成。

为 React 组件编写单元测试是一种常见的做法,通常使用流行的库来测试 React “酶”; 具体来说,它的“浅”方法。 这种方法使我们能够独立于应用程序的其余部分来测试组件。 然而,由于编写 React 应用程序就是组合组件,单靠单元测试并不能确保应用程序没有错误。

例如,更改组件接受的 props 并更新其关联的单元测试可能会导致所有测试通过,而如果另一个组件未相应更新,则应用程序仍可能会损坏。

集成测试有助于在对 React 应用程序进行更改时保持安心,因为它们确保组件的组合产生所需的用户体验。

React 应用集成测试的要求

以下是 React 开发人员在编写集成测试时想要做的一些事情:

  • 从用户的角度测试应用程序用例。 用户访问网页上的信息并与可用控件进行交互。
  • 模拟 API 调用不依赖于 API 可用性和状态来通过/失败测试。
  • 模拟浏览器 API(例如,本地存储),因为它们根本不存在于测试环境中。
  • 断言 React DOM 状态(浏览器 DOM 或原生移动环境)。

现在,在编写 React 应用程序集成测试时,我们应该尽量避免一些事情:

  • 测试实施细节。 实现更改只有在确实引入了错误时才应该中断测试。
  • 嘲讽太多。 我们想测试应用程序的所有部分如何协同工作。
  • 浅渲染。 我们想要测试应用程序中所有组件的组成,直到最小的组件。

为什么选择 React-testing-library?

上述要求使 react-testing-library 成为一个不错的选择,因为它的主要指导原则是允许以类似于实际人类使用方式的方式测试 React 组件。

该库及其可选的配套库允许我们编写与 DOM 交互并断言其状态的测试。

示例应用程序设置

我们将为其编写示例集成测试的应用程序实现了一个简单的场景:

  • 用户输入 GitHub 用户名。
  • 该应用程序显示与输入的用户名关联的公共存储库列表。

从集成测试的角度来看,如何实现上述功能应该是无关紧要的。 然而,为了接近真实世界的应用程序,该应用程序遵循常见的 React 模式,因此该应用程序:

  • 是一个单页应用程序 (SPA)。
  • 发出 API 请求。
  • 具有全局状态管理。
  • 支持国际化。
  • 使用 React 组件库。

应用程序实现的源代码可以在这里找到。

编写集成测试

安装依赖项

用纱线:

 yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock

或者使用 npm:

 npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock

创建集成测试套件文件

我们将在应用程序的./test文件夹中创建一个名为viewGitHubRepositoriesByUsername.spec.js的文件。 Jest 会自动拾取它。

在测试文件中导入依赖

import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test

设置测试套件

describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });

笔记:

  • 在所有测试之前,模拟 GitHub API 以在使用特定用户名调用时返回存储库列表。
  • 每次测试后,清理测试 React DOM,以便每个测试从一个干净的位置开始。
  • describe块指定集成测试用例和流程变化。
  • 我们正在测试的流量变化是:
    • 用户输入具有关联公共 GitHub 存储库的有效用户名。
    • 用户输入没有关联公共 GitHub 存储库的有效用户名。
    • 用户输入 GitHub 上不存在的用户名。
  • it阻止使用异步回调,因为他们正在测试的用例中有异步步骤。

编写第一个流测试

首先,需要渲染应用程序。

 const { getByText, getByPlaceholderText, queryByText } = render(<App />);

@testing-library/react模块导入的render方法在测试 React DOM 中渲染应用程序,并返回绑定到渲染的应用程序容器的 DOM 查询。 这些查询用于定位要与之交互和断言的 DOM 元素。

现在,作为被测流程的第一步,用户将看到一个用户名字段并在其中键入一个用户名字符串。

 userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);

来自导入@testing-library/user-event模块的userEvent助手有一个type方法,可以模仿用户在文本字段中键入文本时的行为。 它接受两个参数:接受输入的 DOM 元素和用户键入的字符串。

用户通常通过与它们关联的文本来查找 DOM 元素。 在输入的情况下,它可以是标签文本或占位符文本。 之前从render返回的getByPlaceholderText查询方法允许我们通过占位符文本找到 DOM 元素。

请注意,由于文本本身经常可能发生变化,因此最好不要依赖实际的本地化值,而是配置本地化模块以返回本地化项目键作为其值。

例如,当“en-US”本地化通常会返回Enter GitHub username作为userSelection.usernamePlaceholder键的值时,在测试中,我们希望它返回userSelection.usernamePlaceholder

当用户在字段中键入文本时,他们应该会看到文本字段值已更新。

 expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);

接下来在流程中,用户单击提交按钮并希望看到存储库列表。

 userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');

userEvent.click方法模拟用户单击 DOM 元素,而getByText查询通过其包含的文本查找 DOM 元素。 closest的修饰符确保我们选择正确种类的元素。

注意:在集成测试中,步骤通常同时服务于actassert角色。 例如,我们断言用户可以通过点击一个按钮来点击它。

在上一步中,我们断言用户看到了应用程序的存储库列表部分。 现在,我们需要断言,由于从 GitHub 获取存储库列表可能需要一些时间,因此用户会看到正在获取的指示。 我们还希望确保应用程序在仍在获取存储库列表时不会告诉用户没有与输入的用户名关联的存储库。

 getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();

请注意, getBy查询前缀用于断言可以找到 DOM 元素,而queryBy查询前缀用于相反的断言。 此外,如果未找到任何元素,则queryBy不会返回错误。

接下来,我们要确保最终应用程序完成获取存储库并将它们显示给用户。

 await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));

waitForElement异步方法用于等待 DOM 更新,该更新将呈现作为方法参数 true 提供的断言。 在这种情况下,我们断言应用程序会显示模拟的 GitHub API 返回的每个存储库的名称和描述。

最后,应用程序不应再显示正在获取存储库的指示符,并且不应显示错误消息。

 expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();

我们生成的 React 集成测试如下所示:

 it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });

备用流量测试

当用户输入没有关联公共存储库的 GitHub 用户名时,应用程序会显示相应的消息。

 describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });

当用户输入不存在的 GitHub 用户名时,应用程序会显示错误消息。

 describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });

为什么 React 集成测试很重要

集成测试确实为 React 应用程序提供了一个甜蜜点。 这些测试有助于捕捉错误并使用 TDD 方法,同时,它们在实现更改时不需要维护。

本文展示的 React-testing-library 是编写 React 集成测试的绝佳工具,因为它允许您像用户一样与应用程序进行交互,并从用户的角度验证应用程序的状态和行为。

希望这里提供的示例将帮助您开始在新的和现有的 React 项目上编写集成测试。 可以在我的 GitHub 上找到包含应用程序实现的完整示例代码。