Повышение ремонтопригодности кода с помощью интеграционного тестирования React

Опубликовано: 2022-03-11

Интеграционные тесты — это золотая середина между стоимостью и ценностью тестов. Написание интеграционных тестов для приложения React с помощью библиотеки react-testing-library вместо модульных тестов компонентов или в дополнение к ним может повысить удобство сопровождения кода без снижения скорости разработки.

Если вы хотите получить фору, прежде чем мы продолжим, вы можете увидеть пример того, как использовать react-testing-library для тестов интеграции приложений React здесь.

Зачем инвестировать в интеграционное тестирование?

«Интеграционные тесты обеспечивают отличный баланс между уверенностью и скоростью/затратами. Вот почему рекомендуется тратить на них большую часть (не все, заметьте) ваших усилий».
- Кент С. Доддс в тестах Write. Не так много. В основном интеграция.

Обычной практикой является написание модульных тестов для компонентов React, часто с использованием популярной библиотеки для тестирования «ферментов» React; в частности, его «поверхностный» метод. Этот подход позволяет нам тестировать компоненты отдельно от остальной части приложения. Однако, поскольку написание приложений React сводится к составлению компонентов, одни только модульные тесты не гарантируют, что приложение не содержит ошибок.

Например, изменение принятых реквизитов компонента и обновление связанных с ним модульных тестов может привести к тому, что все тесты будут пройдены, в то время как приложение может по-прежнему работать с ошибками, если другой компонент не был соответствующим образом обновлен.

Интеграционные тесты могут помочь сохранить душевное спокойствие при внесении изменений в приложение React, поскольку они гарантируют, что композиция компонентов приводит к желаемому UX.

Требования к тестам интеграции приложений 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 i -D jest @testing-library/react @testing-library/user-event jest-dom nock

Создание файла набора тестов интеграции

Мы создадим файл с именем viewGitHubRepositoriesByUsername.spec.js в папке ./test нашего приложения. 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 }); }); });

Примечания:

  • Перед всеми тестами смоделируйте API GitHub, чтобы возвращать список репозиториев при вызове с определенным именем пользователя.
  • После каждого теста очищайте тестовый React DOM, чтобы каждый тест начинался с чистого места.
  • блоки describe определяют вариант использования интеграционного теста и варианты потока.
  • Варианты потока, которые мы тестируем:
    • Пользователь вводит действительное имя пользователя, связанное с общедоступными репозиториями GitHub.
    • Пользователь вводит допустимое имя пользователя, у которого нет связанных общедоступных репозиториев GitHub.
    • Пользователь вводит имя пользователя, которого нет на GitHub.
  • it блокирует использование асинхронного обратного вызова, поскольку тестируемый вариант использования имеет асинхронный шаг.

Написание первого теста потока

Во-первых, приложение должно быть отрендерено.

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

Метод render , импортированный из модуля @testing-library/react , визуализирует приложение в тестовой модели React DOM и возвращает запросы DOM, привязанные к отображаемому контейнеру приложения. Эти запросы используются для поиска элементов DOM для взаимодействия и подтверждения.

Теперь, в качестве первого шага тестируемого потока, пользователю предоставляется поле имени пользователя, и он вводит в него строку имени пользователя.

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

userEvent из импортированного модуля @testing-library/user-event имеет метод type , который имитирует поведение пользователя, когда он вводит текст в текстовое поле. Он принимает два параметра: элемент DOM, который принимает ввод, и строку, которую вводит пользователь.

Пользователи обычно находят элементы DOM по связанному с ними тексту. В случае ввода это либо текст метки, либо текст-заполнитель. Метод запроса getByPlaceholderText , возвращенный ранее из render , позволяет нам найти элемент 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 модификатор гарантирует, что мы выбираем элемент правильного типа.

Примечание. В интеграционных тестах шаги часто выполняют как act , так и assert . Например, мы утверждаем, что пользователь может щелкнуть кнопку, щелкнув ее.

На предыдущем шаге мы утверждали, что пользователь видит раздел списка репозиториев приложения. Теперь нам нужно утверждать, что, поскольку получение списка репозиториев из 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, которое сделает утверждение, указанное в качестве параметра метода, истинным. В этом случае мы утверждаем, что приложение отображает имя и описание для каждого репозитория, возвращаемого фиктивным API GitHub.

Наконец, приложение больше не должно отображать индикатор загрузки репозиториев и не должно отображать сообщение об ошибке.

 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.