В последнем посте мы создали приложение для заказа еды, в котором интегрировали хуки Redux с React.

В этом посте мы узнаем, как писать для него интеграционные тесты с помощью библиотеки тестирования React.

Все обсуждаемые интеграционные тесты доступны на GitHub.

Выберите стратегию тестирования

Для тестирования пользовательского интерфейса у нас есть три варианта: сквозные тесты, интеграционные тесты, модульные тесты. Итак, какую стратегию мы должны использовать? Мне нравится эта идеология от Кента Доддса.

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

В идеальном мире мы бы написали все как тесты E2E, поскольку они больше всего напоминают то, как люди используют наше приложение. Но на практике их сложно настроить и медленно запускать. Поэтому мы сохраняем тесты E2E только для критических пользовательских потоков. (Мы узнаем о тестировании E2E в моей следующей статье)

Что касается всего остального, то следующее лучшее, что мы можем сделать, - это написать интеграционные тесты. Для этого я выбрал библиотеку React Testing Library (также известную как RTL). Вот пример -

Сначала мы визуализируем наш компонент приложения. Затем установите флажок «Только овощи» и, наконец, убедитесь, что в меню отображаются нужные продукты. Прочитав приведенный выше код, сможете ли вы даже определить, использует ли наше приложение React, Redux или хуки? В этом суть интеграционного тестирования. Давайте рассмотрим подробнее, почему этот подход лучше, чем модульное тестирование с неглубоким рендерингом Enzyme.

Мы абстрагируем некоторую логику компонента в отдельный компонент.

Если мы переместим код из компонента в повторно используемый дочерний компонент, наши тесты на основе Enzyme перестанут работать.

Однако интеграционные тесты не заботятся о внутренних деталях компонента. Если мы написали тесты для компонента, все внутри него будет выполнено. Поэтому, если какой-то код перемещается в дочерний компонент, это не имеет значения. Точно так же, как это не имеет значения для пользователей.

При рефакторинге вам не нужно обновлять тесты. Если это произойдет, то какую уверенность дают вам тесты?

Мы меняем имя свойства дочернего компонента

Предположим, что компонент использует внутри компонент Button. Компонент Button имеет свойство onClick, которое вызывается, когда пользователь нажимает кнопку. Если мы используем неглубокий рендеринг для наших компонентных тестов, то внутренние компоненты компонента Button не будут отображаться. Это означает, что если мы имитируем событие щелчка, ничего не произойдет. Вместо этого нам нужно вручную вызвать свойство Button onClick.

Пока это нормально, но посмотрите, что произойдет, когда свойство onClick компонента Button будет переименовано в свойство onPress. Тесты нашего компонента продолжатся, но приложение перестанет работать для реальных пользователей. Это не проблема при написании интеграционных тестов с помощью библиотеки тестирования React.

Это плохо, правда?

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

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

  • Когда рендеринг глубокого дочернего компонента - это головная боль, ничего страшного, если вы над ним издеваетесь. Некоторое издевательство - это нормально по сравнению с издевательством над всем по умолчанию.
  • Модульные тесты отлично подходят для ситуаций, например, если вы хотите убедиться, что служебная функция работает для всех типов входных аргументов.
  • Представьте себе три типа тестов как многослойное сито. E2E выявляет серьезные ошибки, интеграционные тесты - более мелкие, а модульные тесты - выявляют микро-ошибки.

Тестирование нашего приложения для заказа еды

Мы начинаем с написания тестов для нашего самого верхнего компонента, которым является компонент приложения. Точно так же, как мы отрисовали приложение внутри index.js, мы сделаем то же самое в тестах. Это означает, что наше приложение будет упаковано в провайдер Redux и т. Д. Давайте рассмотрим наши интеграционные тесты шаг за шагом.

Индикатор тестовой загрузки

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

я. Мы вызываем функцию renderApp.

Мы создали вспомогательную функцию renderApp. Он обертывает компонент приложения поставщиком Redux и передает ему экземпляр магазина. Мы также разрешаем передавать пользовательские свойства компоненту приложения. Внутри нашего теста мы вызываем renderApp. Это отобразит наш компонент приложения в JS DOM. После этого мы сможем использовать screen для получения данных из визуализированной модели DOM.

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

ii. Мы утверждаем, что отображается заголовок Ordux.

Если вы знакомы с тестированием, то знаете, что мы используем функцию expect, чтобы проверить, соответствует ли полученный результат ожидаемому.

Нам нужно утверждать, что заголовок нашей страницы имеет текст Ordux. Итак, нам нужно извлечь текстовое содержимое заголовка. Но даже до этого нам нужно иметь возможность найти отрисованный заголовочный узел DOM. Вы можете быть удивлены, что мы не используем классы CSS, атрибуты идентификатора теста или имя компонента для поиска заголовочного узла DOM.

Вместо этого мы используем getByRole («заголовок»). Все элементы с h1 по h6 имеют роль заголовка aria, и этот метод использует роли для получения узлов DOM. Обратите внимание, что это похоже на то, как пользователи программ чтения с экрана находят заголовок.

iii. Мы утверждаем, что отображается сообщение о загрузке.

Ранее мы использовали роль заголовка для утверждения. В строке 23 мы используем роль статуса. Но в HTML нет элемента ‹status›, так что же происходит?

Давайте посмотрим, что делает компонент приложения -

useLoadFoodData сообщает приложению текущий статус вызова API. Затем приложение передает статус компоненту сообщения. Когда свойство status равно loading, Сообщение будет отображать живую область aria с ролью status. Таким образом, пользователи программ чтения с экрана будут проинформированы об обновлении некоторого контента через JS.

В некотором смысле RTL заставляет нас активизировать нашу игру в области доступности. Если бы мы просто использовали имя класса для запроса узла DOM, мы могли бы пропустить использование живого региона aria, и пользователи программ чтения с экрана не узнают, что загрузка завершена и что они могут начать взаимодействие с приложением.

Примечание. В нашем тесте выше мы вызываем waitForElementToBeRemoved. Это для фиксации срабатывания предупреждения. Короче говоря, мы говорим React, что обновление состояния, которое приводит к удалению индикатора загрузки, является преднамеренным.

Флажок Test for Veg Only

В нашем приложении пользователи могут установить флажок «Только овощи», и он покажет им только вегетарианские блюда. Давайте посмотрим, как мы можем проверить этот поток.

я. Мы имитируем вызов API с помощью шутки.

Когда мы пишем интеграционные тесты, мы часто имитируем вызовы API, чтобы наш конвейер тестирования CI оставался простым, а также работал быстро. Это также позволяет нам моделировать такие ситуации, как API-интерфейс работает слишком долго или дает неправильный ответ.

Есть несколько способов имитировать наши API. Кент Доддс предпочитает MSW, но в этой публикации мы просто имитируем loadFoodData утилиту, чтобы она не выполняла вызовы API. Внутри beforeEach мы имитируем реализацию loadFoodData, чтобы обещание разрешалось со статическими данными. Внутри afterEach мы восстанавливаем утилиту, чтобы другой тест мог получить доступ к реальной утилите loadFoodData, если это необходимо.

Затем мы визуализируем компонент App и ждем, пока индикатор загрузки не будет удален из DOM. Когда это происходит, мы знаем, что загрузка API завершена и наше меню будет видно.

ii. События клика-триггера.

Теперь нам нужно установить флажок Veg Only. Мы находим узел DOM флажка с помощью getByRole. Если флажков несколько, мы можем настроить таргетинг на конкретный, сопоставив его имя. Затем мы устанавливаем флажок, используя fireEvent.click.

iii. Убедитесь, что при включении фильтра Veg Only элементы скрываются.

После того, как мы установим флажок, из двух продуктов мы должны увидеть только Veg. Чтобы утверждать это, нам нужно получить узлы DOM для двух продуктов питания. Здесь мы будем использовать queryByText и getByText. Точно так же, как люди находят материал, читая текст, мы находим узлы DOM, используя написанный в нем текст.

Но зачем нам два разных метода? Причина в том, что getByText вызывает ошибку, если не находит ни одного подходящего узла DOM. Это полезно, когда мы ожидали, что узел DOM присутствует, но его нет. Обратите внимание, что мы использовали getByText вместе с toBeInTheDocument. Однако, когда мы хотим утверждать, что узел DOM отсутствует, мы используем queryByText. Он не выдаст ошибку, а просто вернет null.

В строке 43 мы проверяем, что когда мы отключаем фильтр, он снова начинает показывать два элемента. На этом мы закончили тестирование всего потока фильтрации Veg Only.

Если вы хотите увидеть еще более крупный пример интеграционного тестирования, отметьте этот тест на GitHub. Он проверяет, что когда мы добавляем и удаляем товары из корзины, отображается правильное количество, а окончательная цена в нижнем колонтитуле также обновляется.

Краткое изложение подхода к тестированию

То, как я пишу тесты, можно резюмировать так:

  • Я начинаю с самого верхнего компонента App, визуализирую его как root, а затем тестирую все пользовательские потоки. Не проблема, если тестовый блок становится длинным.
  • Я визуализирую компонент внутри каждого теста вместо beforeEach, потому что я могу захотеть настроить что-то в тесте перед рендерингом или передать дополнительные свойства компоненту при его монтировании.
  • Я нацелен на узлы DOM, используя их роли aria или текстовое содержимое, поскольку это вещи, которые важны для пользователя. Пользователь нажимает * кнопку * с текстом * Отправить *. Им не сообщаются классы или идентификаторы на кнопке.
  • Если есть крайние случаи, не охваченные обычными пользовательскими потоками, я пишу для них отдельные тесты, и обычно они остаются небольшими.
  • Дочерние компоненты могут делать дополнительные вещи, о которых Приложение не заботится. Их можно протестировать в отдельном тесте, где дочерний компонент отображается как root.

Заключение

В этой статье мы узнали, как выбрать стратегию тестирования, имитировать вызовы API и написать интеграционные тесты для всех пользовательских потоков. Но есть еще один сюрприз.

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

Если вам понравилась статья, нажмите кнопку хлопка. Это меня действительно мотивирует. Если у вас есть какие-либо вопросы или вы хотите быть в курсе последних новостей, подписывайтесь на меня в Twitter.

Учить больше