[Перевод] Обзор библиотеки react-testing-library

В материале, перевод которого мы публикуем сегодня, Кент Доддс рассказывает о библиотеке собственной разработки для тестирования React-приложений, react-testing-library, в которой он видит простой инструмент, способный заменить enzyme и способствующий написанию качественных тестов с применением передовых наработок в этой области.

2ddnwflswfc-8g9qp-3woxddwku.jpeg

Автор материала говорит, что давно размышлял о чём-то подобном, и в итоге, примерно в середине прошлого месяца, решил заняться разработкой библиотеки для тестирования, которая его устраивала бы. В частности, в enzyme ему не нравилось то, что большинство возможностей этой библиотеки склоняют разработчика к не самым лучшим методам подготовки тестов, которые способны навредить проекту. В результате у него получился простой, но самодостаточный набор инструментов для тестирования React DOM.

Общие сведения о библиотеке react-testing-library


Предположим, вы собираетесь писать тесты для компонентов React. Вы хотите, чтобы эти тесты было удобно поддерживать. Кроме того, вам надо, чтобы тесты не опирались на детали реализации компонентов и испытывали компоненты в условиях, приближённых к реальным сценариям их использования. Кроме того, вы стремитесь к тому, чтобы тесты оставались бы рабочими в долгосрочной перспективе, то есть, хотите, чтобы рефакторинг компонентов (то есть — изменение их реализации, но не функционала) не нарушал бы систему тестирования и не заставлял бы вас или вашу команду постоянно переписывать тесты, замедляя работу над проектом.

Что выбрать для достижения этих целей? В нашем случае ответом на эти вопросы стала библиотека react-testing-library, минималистичное решение, предназначенное для тестирования компонентов React.

d2307b80cdfb223a3f777cdcc75148bc.png


Логотип react-testing-library

Эта библиотека даёт разработчику простые инструменты, построенные на базе react-dom и react-dom/test-utild, причём, библиотека устроена так, чтобы тот, кто пользуется ей, без особых проблем применял бы в своей работе передовые практики тестирования. В основе react-testing-library лежит следующий принцип: чем больше процесс тестирования напоминает реальный сеанс работы с приложением — тем увереннее можно говорить о том, что, когда приложение попадёт в продакшн, оно будет работать так, как ожидается.

В результате вместо того, чтобы работать с экземплярами отрендеренных компонентов React, тесты будут взаимодействовать с реальными узлами DOM. Механизмы, предоставляемые библиотекой, выполняют обращения к DOM таким же образом, каким это бы делали пользователи проекта. Поиск элементов осуществляется по текстам их меток (так поступают и пользователи), поиск ссылок и кнопок так же происходит по их текстам (и это характерно для пользователей). Кроме того, здесь, в качестве «запасного выхода», есть возможность применять поиск элементов по data-testid. Это — распространённая практика, позволяющая работать с элементами, подписи которых не имеют смысла или непрактичны для данной цели.

Рассматриваемая библиотека способствует повышению доступности приложений, помогает приблизить тесты к реальным сценариям работы.

Библиотека react-testing-library является заменой для enzyme. Используя enzyme, можно следовать тем же принципам, которые заложены в рассматриваемой здесь библиотеке, но в этом случае их сложнее придерживаться из-за дополнительных средств, предоставляемых enzyme (то есть, всего того, что помогает в деталях реализации тестов). Подробности об этом можно почитать здесь.

Кроме того, хотя react-testing-library предназначена для react-dom, она подходит и для React Native благодаря использованию этого небольшого файла настроек.

Сразу стоит сказать, о том, что эта библиотека не является средством для запуска тестов или фреймворком. Кроме того, она не привязана к некоему фреймворку для тестирования (хотя мы и рекомендуем Jest, но это всего лишь инструмент, которым мы предпочитаем пользоваться, в целом же библиотека будет работать с любым фреймворком и даже в среде CodeSandbox!).

Практический пример


Рассмотрим следующий код, демонстрирующий практический пример работы с react-testing-library.

import React from 'react'
import {render, Simulate, wait} from 'react-testing-library'
// Тут добавляется средство проверки ожиданий
import 'react-testing-library/extend-expect'
// Mock-объект находится в директории __mocks__
import axiosMock from 'axios'
import GreetingFetcher from '../greeting-fetcher'
test('displays greeting when clicking Load Greeting', async () => {
  // Этап Arrange
  axiosMock.get.mockImplementationOnce(({name}) =>
    Promise.resolve({
      data: {greeting: `Hello ${name}`}
    })
  )
  const {
    getByLabelText,
    getByText,
    getByTestId,
    container
  } = render()
  // Этап Act
  getByLabelText('name').value = 'Mary'
  Simulate.click(getByText('Load Greeting'))
  // Подождём разрешения mock-запроса `get`
  // Эта конструкция будет ждать до тех пор, пока коллбэк не выдаст ошибку
  await wait(() => getByTestId('greeting-text'))
  // Этап Assert
  expect(axiosMock.get).toHaveBeenCalledTimes(1)
  expect(axiosMock.get).toHaveBeenCalledWith(url)
  // собственный матчер!
  expect(getByTestId('greeting-text')).toHaveTextContent(
    'Hello Mary'
  )
  // снэпшоты отлично работают с обычными узлами DOM!
  expect(container.firstChild).toMatchSnapshot()
})


Самое важное, что можно вынести из этого примера, заключается в том, что тесты напоминают работу с приложением реального пользователя.

Продолжим анализировать этот код.

GreetingFletcher может вывести некий HTML-код, например, такой:

         


При работе с этими элементами ожидается следующая последовательность действий: задать имя, щёлкнуть по кнопке Load Greeting, что вызовет запрос к серверу для загрузки некоего текста, в котором использовано заданное имя.

В тесте понадобится найти поле , в результате можно будет установить его параметр value в некое значение. Здравый смысл подсказывает, что тут можно использовать свойство id в CSS-селекторе: #name-input. Но так ли поступает пользователь для того, чтобы найти поле ввода? Определённо — не так! Пользователь смотрит на экран и находит поле с подписью Name, в которое он вводит данные. Поэтому именно так поступает наш тест с getByLabelText. Он обнаруживает элемент управления, основываясь на его метке.

Часто в тестах, созданных на основе enzyme, для поиска кнопки, например, с надписью Load Greeting, используется CSS-селектор или производится поиск по displayName конструктора компонента. Но когда пользователь хочет загрузить некий текст с сервера, он не думает о деталях реализации программы, вместо этого он ищет кнопку с надписью Load Greetings и щёлкает по ней. Именно это и делает наш тест с помощью вспомогательной функции getByText.

В дополнение к этому, конструкция wait, опять же, имитирует поведение пользователя. Тут организовано ожидание появления текста на экране. Система будет ждать столько, сколько нужно. В нашем тесте для этого используется mock-объект, поэтому вывод текста происходит практически мгновенно. Но наш тест не заботится о том, сколько времени это займёт. Нам не нужно использовать в тесте setTimeout или что-то подобное.

Мы просто сообщаем тесту: «Подожди появления узла greeting-text». Обратите внимание на то, что в данном случае используется атрибут data-testid, тот самый «запасной выход», применяемый в ситуациях, когда искать элементы, пользуясь каким-то другим механизмом, не имеет смысла. В подобных случаях data-testid определённо лучше, чем альтернативные методы.

Общий обзор API


В самом начале библиотека давала разработчику лишь метод queryByTestId. Почитать об этом можно здесь. Однако, благодаря отклику на вышеупомянутую публикацию и этому фантастическому выступлению, в библиотеку были добавлены дополнительные методы.

Подробности о библиотеке и о её API можно почитать в официальной документации. Здесь же приведён общий обзор её возможностей.

  • Simulate — реэкспорт из вспомогательного средства Simulate react-dom/test-utils.
  • wait — позволяет организовать в тестах ожидание в течение неопределённого периода времени. Обычно следует применять mock-объекты для запросов к API или анимаций, но даже при работе с немедленно разрешаемыми промисами, нужно, чтобы тесты ждали следующего тика цикла событий. Метод wait отлично для этого подходит. (Огромное спасибо Лукашу Гандецки, который предложил это в качестве замены для API flushPromises, которое теперь считается устаревшим).


  • render: тут скрыта вся суть библиотеки. На самом деле, это — довольно простая функция. Она создаёт элемент div с помощью document.createElement, затем использует ReactDOM.render для вывода данных в этот div.


Функция render возвращает следующие объекты и вспомогательные функции:

  • container: элемент div, в который был выведен компонент.
  • unmount: простая обёртка вокруг ReactDOM.unmountComponentAtNode для размонтирования компонента (например, для упрощения тестирования componentWillUnmount).
  • getByLabelText: получает элемент управления формы, основываясь на его метке.
  • getByPlaceholderText: плейсхолдеры — это не слишком хорошие альтернативы меткам, но если это имеет смысл в конкретной ситуации, можно воспользоваться и ими.
  • getByText: получает элемент по его текстовому содержимому.
  • getByAltText: получает элемент (вроде) по значению его атрибута alt.
  • getByTestId: получает элемент по его атрибуту data-testid.


Каждый из этих вспомогательных get-методов выводит информативное сообщение об ошибке если элемент найти не удаётся. Кроме того, тут имеется и набор аналогичных query-методов (вроде queryByText). Они, вместо выдачи ошибки при отсутствии элемента, возвращают null, что может быть полезно в случае, если нужно проверить DOM на отсутствие элемента.

Кроме того, этим методам, для поиска нужного элемента, можно передавать следующее:

  • Нечувствительную к регистру строку: например, lo world будет соответствовать Hello World.
  • Регулярное выражение: например, /^Hello World$/ будет соответствовать Hello World.
  • Функцию, которая принимает текст и элемент: например, использование функции (text, el) => el.tagName === 'SPAN' && text.startsWith('Hello') приведёт к выбору элемента span, текстовое содержимое которого начинается с Hello.


Надо отметить, что благодаря Энто Райену в библиотеке имеются собственные матчеры для Jest.

  • toBeInTheDOM: проверяет, присутствует ли элемент в DOM.
  • toHaveTextContent: проверяет, имеется ли в заданном элементе текстовое содержимое.


Итоги


Основная особенность библиотеки react-testing-library заключается в том, что у неё нет вспомогательных методов, которые позволяют тестировать детали реализации компонентов. Она направлена на разработку тестов, которые способствуют применению передовых практик в области тестирования и разработки ПО. Надеемся, библиотека react-testing-library вам пригодится.

Уважаемые читатели! Планируете ли вы использовать react-testing-library в своих проектах?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru