7分钟阅读

提高代码可维护性与React Integration测试

安东是一个全堆叠开发人员,具有强大的技术背景。他专门从事JavaScript,他是一个测试驱动的发展的粉丝。

集成试验是测试成本和价值之间的甜蜜点。在反应测试库的帮助下为React应用程序而不是或除组件单元测试之外的编写集成测试可以增加代码可维护性而不会损害开发速度。

如果您希望在我们继续之前开始一个头部,您可以看到如何使用React-Testing-Library进行React App Integration Tests的示例 这里 .

为什么投资整合测试?

“集成试验在信心和速度/费用之间的权衡方面取得了很大的平衡。这就是为什么建议花费大多数(不是全部,介意你)你的努力。”
– Kent C. Dodds in 写测试。不是很多。主要是集成。

写入对反应组件的单位测试是一种常见的做法,通常使用流行的测试React“酶”库;具体而言,其“浅”方法。此方法允许我们从应用程序的其余部分隔离测试组件。但是,由于写作React应用程序都是关于构图组件的,因此单独的单元测试不确保该应用程序是无窃听的。

例如,更改组件的已接受的道具和更新其相关单元测试可能导致所有测试传递,而在应用程序仍然会被损坏,则如果未更新另一个组件,则可能仍然被损坏。

集成测试可以帮助保持对抗应用程序的改变的同时保持安心,因为它们确保组分的组成导致所需的UX。

对React App Integration测试的要求

以下是一些事情 反应开发人员 在编写集成测试时执行以下操作:

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

现在,对于我们应该尝试的一些事情 避免 在编写React App Integration测试时:

  • 测试实施细节。实施变更应该只打破测试,如果他们确实引入了错误。
  • 模拟太多了。我们想测试应用程序的所有部分如何一起工作。
  • 浅渲染。我们希望将App中所有组件的组成测试到最小组件。

为什么选择反应测试 - 库?

上述要求 反应测试 - 库 作为其主要指导原理的伟大选择是允许以类似的方式测试的反应组件,这是一种类似于实际人类使用的方式。

图书馆以及其可选 伴侣图书馆,允许我们编写与DOM交互的测试并在其状态上断言。

示例应用程序设置

我们要编写样本集成测试的应用程序实现了一个简单的方案:

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

如何实现上述功能,应与集成测试视角无关。但是,要保持靠近真实世界的应用程序,该应用程序遵循常见的反应模式,因此应用程序:

  • 是单页应用程序(spa)。
  • 制作API请求。
  • 有全球国家管理。
  • 支持国际化。
  • 利用反应组件库。

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

编写集成测试

安装依赖项

用纱线:

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

创建集成测试套件文件

We will create a file named viewGitHubRepositoriesByUsername.spec.js file in the ./test folder of our application. Jest will automatically pick it up.

导入测试文件中的依赖项

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('//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模拟,以在使用特定用户名调用时返回存储库列表。
  • 每次测试后,清洁测试反应DOM,使每个测试从干净的位置开始。
  • describe 块指定集成测试用例和流动变化。
  • 我们测试的流动变化是:
    • 用户输入具有关联的公共GitHub存储库的有效用户名。
    • 用户输入没有关联的公共GitHub存储库的有效用户名。
    • 用户进入GitHub上不存在的用户名。
  • it 块使用异步回调作为它们在测试中的用例具有异步步骤。

写第一流测试

首先,需要呈现应用程序。

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

The render method imported from the @testing-library/react module renders the app in the test React DOM and returns DOM queries bound to the rendered app container. These queries are used to locate DOM elements to interact with and to assert on.

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

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

The userEvent helper from imported @testing-library/user-event module has a type method that imitates the behavior of the user when they type text into a text field. It accepts two parameters: the DOM element that accepts the input and the string that the user types.

Users usually find DOM elements by the text associated with them. In the case of input, it is either label text or placeholder text. The getByPlaceholderText query method returned earlier from render allows us to find the DOM element by placeholder text.

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

For example, when “en-US” localization would normally return Enter GitHub username as the value for the userSelection.usernamePlaceholder key, in tests, we want it to return userSelection.usernamePlaceholder.

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

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

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

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

The userEvent.click method imitates the user clicking on a DOM element, while getByText query finds a DOM element by the text it contains. The closest modifier ensures that we select the element of the right kind.

笔记: In integration tests, steps often serve both act and assert roles. For example, we assert that the user can click a button by clicking it.

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

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

Note that the getBy query prefix is used to assert that the DOM element can be found and the queryBy query prefix is useful for the opposite assertion. Also, queryBy does not return an error if no element is found.

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

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

The waitForElement asynchronous method is used to wait for a DOM update that will render the assertion provided as method parameter true. In this case, we assert that the app displays the name and description for every repository returned by the mocked GitHub API.

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

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

我们所产生的反应集成测试如下所示:

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 Integration Tests Rock

集成测试真正为反应应用提供甜蜜点。这些测试有助于捕获错误并使用 TDD方法 同时,当实现更改时,它们不需要维护。

Reft-Test-Mountruct-Library,在本文中展示,是用于编写React Integration Tests的一个很好的工具,因为它允许您在用户身份和用户的角度下验证应用程序状态和行为和验证应用程序状态和行为。

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

理解基础知识

什么是反应测试?

它是用反应库写入的用户界面的自动测试。

软件测试中的集成测试是什么?

集成测试是一种自动测试的变体,旨在确保系统的单独组件正在共同努力满足用户的目标。

单元测试和集成测试之间有什么区别?

集成测试确保组件函数在一起,而单位测试确保每个组件都函数自己,但这并不能保证组件集成中没有问题。

什么是端到端测试?

端到端测试是自动测试的变体,旨在确保所有系统都在一起履行用户的目标。

你为什么要使用反应?

这是一个基于组件的基于组件的基于组件的库,用于构建用户界面。