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


Автор материала говорит, что давно размышлял о чём-то подобном, и в итоге, примерно в середине прошлого месяца, решил заняться разработкой библиотеки для тестирования, которая его устраивала бы. В частности, в 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 отлично для этого подходит. (Огромное спасибо Лукашу Гандецки, который предложил это в качестве замены для 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 в своих проектах?

Комментарии (8)


  1. k12th
    12.04.2018 12:28

    Черный козёл на КДПВ какбэ намекает, какой чёрной магией предстоит заняться читателю...


    1. ru_vds Автор
      12.04.2018 15:42

      Поменяли картинку


      1. k12th
        12.04.2018 15:43

        Про эту я тоже могу пошутить, но не буду вас мучить:)


  1. shir
    12.04.2018 12:35

    Вот с тех пор как начал писать что-то с использованием реакта (да и вообще фронтенд SPA на JS) столкнулся с тем что нет библиотек для функционального тестирования. Вот это первое что нравится (но далеко не идеальное). В идеале хочется capybara на js.


  1. FoterIS
    12.04.2018 13:52

    Чем-то Selenium напомнило. Только тестируется не весь сайт. А только конкретный компонент


  1. vintage
    13.04.2018 09:56

    Автор не совсем прав или совсем не прав. Пользователь не ищет поле по тексту «Имя», чтобы ввести имя. Пользователь сканирует элементы и видит запрос имени. Сам текст запроса может быть любым: «Имя», «Введите имя», «Ваше имя», «Name» или вообще иконка. Текстовые константы имеют свойство меняться, не влияя на функциональность. Поэтому, если мы завязываемся на выводимые пользователю тексты, то получаем хрупкие тесты.

    Далее, так как дерево компонент рендерится целиком, то тесты более высокоуровнего компонента начинают зависеть от деталей реализации низкоуровневых. Это приводит к тому, что рефакторинг любого компонента (без изменения его апи) с весьма не нулеевой вероятностью ломает тесты всех косвенно использующих его компонент. То есть тесты опять же получаются хрупкими.

    Когда у вас несколько элементов с одинаковыми текстами, вообще начинается цирк с конями.

    Вцелом, если вы берётесь эмулировать действия пользователя, то имейте ввиду, что пользователь взаимодействует не с отдельным компонентом, а с приложением целиком. То есть тогда надо писать e2e тесты через тот же селениум.


    1. justboris
      13.04.2018 19:44

      Вы статью внимательно прочли? Для таких случаев предлагается использовать data-testid и функцию getByTestId.


      1. vintage
        13.04.2018 22:13

        О том и речь, что нет случаев, когда стоит использовать что-то кроме неё. И правильный фреймворк генерирует эти идентификаторы автоматически на основе семантики.