Автор материала говорит, что давно размышлял о чём-то подобном, и в итоге, примерно в середине прошлого месяца, решил заняться разработкой библиотеки для тестирования, которая его устраивала бы. В частности, в enzyme ему не нравилось то, что большинство возможностей этой библиотеки склоняют разработчика к не самым лучшим методам подготовки тестов, которые способны навредить проекту. В результате у него получился простой, но самодостаточный набор инструментов для тестирования React DOM.
Общие сведения о библиотеке react-testing-library
Предположим, вы собираетесь писать тесты для компонентов React. Вы хотите, чтобы эти тесты было удобно поддерживать. Кроме того, вам надо, чтобы тесты не опирались на детали реализации компонентов и испытывали компоненты в условиях, приближённых к реальным сценариям их использования. Кроме того, вы стремитесь к тому, чтобы тесты оставались бы рабочими в долгосрочной перспективе, то есть, хотите, чтобы рефакторинг компонентов (то есть — изменение их реализации, но не функционала) не нарушал бы систему тестирования и не заставлял бы вас или вашу команду постоянно переписывать тесты, замедляя работу над проектом.
Что выбрать для достижения этих целей? В нашем случае ответом на эти вопросы стала библиотека
react-testing-library
, минималистичное решение, предназначенное для тестирования компонентов React.Логотип 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(<GreetingFetcher />)
// Этап 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-код, например, такой:<div>
<label for="name-input">Name</label>
<input id="name-input" />
<button>Load Greeting</button>
<div data-testid="greeting-text"></div>
</div>
При работе с этими элементами ожидается следующая последовательность действий: задать имя, щёлкнуть по кнопке
Load Greeting
, что вызовет запрос к серверу для загрузки некоего текста, в котором использовано заданное имя.В тесте понадобится найти поле
<input />
, в результате можно будет установить его параметр 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
отлично для этого подходит. (Огромное спасибо Лукашу Гандецки, который предложил это в качестве замены для APIflushPromises
, которое теперь считается устаревшим).
- 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 в своих проектах?
Комментарии (8)
shir
12.04.2018 12:35Вот с тех пор как начал писать что-то с использованием реакта (да и вообще фронтенд SPA на JS) столкнулся с тем что нет библиотек для функционального тестирования. Вот это первое что нравится (но далеко не идеальное). В идеале хочется capybara на js.
FoterIS
12.04.2018 13:52Чем-то Selenium напомнило. Только тестируется не весь сайт. А только конкретный компонент
vintage
13.04.2018 09:56Автор не совсем прав или совсем не прав. Пользователь не ищет поле по тексту «Имя», чтобы ввести имя. Пользователь сканирует элементы и видит запрос имени. Сам текст запроса может быть любым: «Имя», «Введите имя», «Ваше имя», «Name» или вообще иконка. Текстовые константы имеют свойство меняться, не влияя на функциональность. Поэтому, если мы завязываемся на выводимые пользователю тексты, то получаем хрупкие тесты.
Далее, так как дерево компонент рендерится целиком, то тесты более высокоуровнего компонента начинают зависеть от деталей реализации низкоуровневых. Это приводит к тому, что рефакторинг любого компонента (без изменения его апи) с весьма не нулеевой вероятностью ломает тесты всех косвенно использующих его компонент. То есть тесты опять же получаются хрупкими.
Когда у вас несколько элементов с одинаковыми текстами, вообще начинается цирк с конями.
Вцелом, если вы берётесь эмулировать действия пользователя, то имейте ввиду, что пользователь взаимодействует не с отдельным компонентом, а с приложением целиком. То есть тогда надо писать e2e тесты через тот же селениум.justboris
13.04.2018 19:44Вы статью внимательно прочли? Для таких случаев предлагается использовать
data-testid
и функциюgetByTestId
.vintage
13.04.2018 22:13О том и речь, что нет случаев, когда стоит использовать что-то кроме неё. И правильный фреймворк генерирует эти идентификаторы автоматически на основе семантики.
k12th
Черный козёл на КДПВ какбэ намекает, какой чёрной магией предстоит заняться читателю...
ru_vds Автор
Поменяли картинку
k12th
Про эту я тоже могу пошутить, но не буду вас мучить:)