Сейчас существует большая путаница относительно «правильного» способа тестирования компонентов React. Следует ли писать все тесты вручную или использовать только снимки состояния, или некоторые из них? Стоит ли тестировать реквизит? Состояние? Стили / макет?

Я не думаю, что есть один «правильный» путь, но я нашел несколько шаблонов и советов, которые действительно работают для меня, и я хотел бы поделиться ими.

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

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

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

Предыстория: приложение, которое мы будем тестировать

Предположим, вы хотите протестировать LockScreen компонент, который ведет себя как экран блокировки телефона. Это:

  • Показывает текущее время
  • Может показывать определяемое пользователем сообщение
  • Может показывать заданное пользователем фоновое изображение
  • Внизу виджет слайд-разблокировки.

Выглядит это примерно так:

Вы можете попробовать это здесь, а просмотреть код на GitHub.

Вот код для App компонента верхнего уровня:

Как видите, LockScreen получает три свойства: wallpaperPath, userInfoMessage и onUnlocked.

Вот код для LockScreen:

LockScreen включает несколько других компонентов, но, поскольку мы только тестируем LockScreen, давайте сосредоточимся на нем прямо сейчас.

Компонентные контракты

Чтобы протестировать LockScreen, вы должны сначала понять, что это за Контракт. Понимание контракта компонента - самая важная часть тестирования компонента React. Контракт определяет ожидаемое поведение вашего компонента и разумные предположения относительно его использования. Без четкого контракта ваш компонент может быть трудно понять. Написание тестов - отличный способ формально определить контракт вашего компонента.

У каждого компонента React есть по крайней мере одна вещь, которая способствует определению его контракта:

  • Что он отображает (что может быть ничем)

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

  • свойства, которые получает компонент
  • Состояние, в котором находится компонент (если есть)
  • Что делает компонент, когда пользователь взаимодействует с ним (посредством нажатия, перетаскивания, ввода с клавиатуры и т. Д.)

Менее распространенные вещи, влияющие на контракты компонентов:

  • контекст, в котором отображается компонент
  • Что делает компонент, когда вы вызываете методы в его экземпляре (интерфейс общедоступной ссылки)
  • Побочные эффекты, возникающие в рамках жизненного цикла компонента (componentDidMount, componentWillUnmount и т. д.)

Чтобы найти контракт на ваш компонент, задайте себе такие вопросы:

  • Что отображает мой компонент?
  • Отрисовывает ли мой компонент разные вещи при разных обстоятельствах?
  • Когда я передаю функцию в качестве опоры, для чего мой компонент ее использует? Вызывает ли она ее или просто передает ее другому компоненту? Если он это называет, то как он это называет?
  • Что происходит, когда пользователь взаимодействует с моим компонентом?

Поиск контракта LockScreen

Давайте рассмотрим метод LockScreen render и добавим комментарии в тех местах, где его поведение может отличаться. В качестве подсказок вы будете искать троичные операторы, операторы if и операторы switch. Это поможет нам найти вариации в его контракте.

Мы узнали три ограничения, которые описывают контракт LockScreen:

  • Если передается wallpaperPath prop, внешняя оболочка div, которую визуализирует компонент, должна иметь свойство background-image CSS во встроенных стилях, установленное на любое значение wallpaperPath, заключенное в url(...).
  • Если передано свойство userInfoMessage, оно должно быть передано как дочерний элемент TopOverlay, который должен отображаться с определенным набором встроенных стилей.
  • Если свойство userInfoMessage не передано, должно отображаться no TopOverlay.

Вы также можете найти некоторые ограничения контракта, которые всегда верны:

  • Всегда отображается div, который содержит все остальное. Он имеет определенный набор встроенных стилей.
  • Всегда отображается ClockDisplay. Никаких реквизитов он не получает.
  • Всегда отображается SlideToUnlock. Он получает значение переданной onUnlocked опоры как ее onSlide опоры, независимо от того, было оно определено или нет.

propTypes компонента также является хорошим местом для поиска подсказок о его контракте. Я заметил еще несколько ограничений:

  • wallpaperPath должен быть строкой и не является обязательным.
  • Ожидается, что userInfoMessage будет строкой и не является обязательным.
  • Ожидается, что onUnlocked будет функцией и не является обязательным.

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

Что стоит проверить?

Давайте посмотрим на найденный нами контракт:

  • Ожидается, что wallpaperPath будет строкой и не является обязательным.
  • userInfoMessage должен быть строкой и не является обязательным.
  • Ожидается, что onUnlocked будет функцией и не является обязательным.
  • Всегда отображается div, который содержит все остальное. Он имеет определенный набор встроенных стилей.
  • Всегда отображается ClockDisplay. Никаких реквизитов он не получает.
  • Всегда отображается SlideToUnlock. Он получает значение переданного onUnlocked prop как его onSlide prop, независимо от того, было оно определено или нет.
  • Если передано свойство wallpaperPath, самый внешний оборачивающий div, который визуализирует компонент, должен иметь свойство background-image css во встроенных стилях, установленное на любое значение wallpaperPath, заключенное в url(...).
  • Если передано свойство userInfoMessage, оно должно быть передано как дочерний элемент TopOverlay, который должен отображаться с определенным набором встроенных стилей.
  • Если свойство userInfoMessage не передано, должно отображаться no TopOverlay.

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

  1. Придется ли тесту дублировать в точности код приложения? Это сделает его хрупким.
  2. Будет ли выполнение утверждений в тесте дублировать любое поведение, которое уже охватывается (и входит в обязанности) библиотечного кода?
  3. С точки зрения стороннего наблюдателя, эта деталь важна или это только внутренняя забота? Можно ли описать влияние этой внутренней детали, используя только общедоступный API компонента?

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

Давайте рассмотрим наши ограничения и воспользуемся этими практическими правилами, чтобы определить, какие из них необходимо протестировать. Вот первые три:

  • Ожидается, что wallpaperPath будет строкой и не является обязательным.
  • userInfoMessage должен быть строкой и не является обязательным.
  • Ожидается, что onUnlocked будет функцией и не является обязательным.

Эти ограничения относятся к PropTypes механизму React, и поэтому написание тестов для типов опор не соответствует правилу № 2 (уже охваченному кодом библиотеки). Поэтому я не тестирую типы опор. Поскольку тесты часто дублируются как документация, я мог бы решить протестировать что-то, что не соответствует правилу № 2, если код приложения не очень хорошо документирует ожидаемые типы, но propTypes уже приятны и удобочитаемы.

Вот следующее ограничение:

  • Всегда отображается div, который содержит все остальное. Он имеет определенный набор встроенных стилей.

Это можно разбить на три ограничения:

  • Всегда отображается div.
  • Рендеринг div содержит все остальное, что рендерится.
  • Визуализированный div имеет определенный набор встроенных стилей.

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

Игнорируя свойство background-image, на которое накладывается другое ограничение, упаковка div имеет следующие стили:

height: "100%",
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
backgroundColor: "black",
backgroundPosition: "center",
backgroundSize: "cover",

Если бы мы написали тест на наличие этих стилей в div, нам пришлось бы проверять значение каждого стиля точно, чтобы делать полезные утверждения. Итак, наши утверждения могут быть примерно такими:

  • Оберточный div должен иметь свойство стиля высоты 100%.
  • Оберточный div должен иметь свойство стиля отображения flex.
  • … И так далее для каждого свойства стиля

Даже если бы мы использовали что-то вроде toMatchObject, чтобы сделать этот тест лаконичным, это дублировало бы те же стили в коде приложения и было бы нестабильным. Если бы мы добавили другой стиль, нам пришлось бы использовать тот же код в нашем тесте. Если бы мы изменили стиль, нам пришлось бы настроить его в нашем тесте, даже если поведение компонента могло не измениться. Следовательно, это ограничение не соответствует правилу № 1 (дублирует код приложения; нестабильно). По этой причине я не тестирую встроенные стили, если они не могут измениться во время выполнения.

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

Вот следующие два ограничения:

  • Всегда отображается ClockDisplay. Никаких реквизитов он не получает.
  • Всегда отображается SlideToUnlock. Он получает значение переданного onUnlocked пропа как своего onSlide пропа, независимо от того, было оно определено или нет.

Их можно разбить на:

  • Всегда отображается ClockDisplay.
  • Рендеринг ClockDisplay не получает никаких реквизитов.
  • Всегда отображается SlideToUnlock.
  • Когда переданная onUnlocked опора определена, обработанная SlideToUnlock получает значение этой опоры в качестве своего onSlide опоры.
  • Когда переданное свойство onUnlocked равно undefined, свойство onSlide отрендеренного SlideToUnlock также должно быть установлено на undefined.

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

Следующее ограничение:

  • Если передано свойство wallpaperPath, самый внешний оборачивающий div, который визуализирует компонент, должен иметь свойство background-image css во встроенных стилях, установленное на любое значение wallpaperPath, заключенное в url(...).

Вы можете подумать, что, поскольку это встроенный стиль, нам не нужно его тестировать. Однако, поскольку значение background-image может изменяться в зависимости от wallpaperPath prop, его необходимо протестировать. Если бы мы не тестировали его, то не было бы никакого теста на влияние свойства wallpaperPath, которое часть публичного интерфейса этого компонента. Вы всегда должны тестировать свой общедоступный интерфейс.

Последние два ограничения:

  • Если передано свойство userInfoMessage, оно должно быть передано как дочерний элемент TopOverlay, который должен отображаться с определенным набором встроенных стилей.
  • Если свойство userInfoMessage не передано, должно отображаться no TopOverlay.

Их можно разбить на:

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

Первое и четвертое ограничения (TopOverlay должны / не должны отображаться) описывают то, что мы отображаем, поэтому мы их протестируем.

Второе ограничение проверяет, что TopOverlay получает конкретную опору на основе значения userInfoMessage. Важно написать тесты для свойств, которые получают визуализируемые компоненты, поэтому мы их протестируем.

Третье ограничение проверяет, что TopOverlay получает конкретную опору, поэтому вы можете подумать, что мы должны ее протестировать. Однако эта опора - всего лишь несколько встроенных стилей. Утверждение о том, что реквизиты передаются, важно, но утверждение о встроенных стилях непостоянно и дублирует код приложения (не соответствует правилу № 1). Поскольку важно проверять пройденные реквизиты, неясно, следует ли это проверять, просто рассматривая одно правило №1; К счастью, именно поэтому у меня есть правило №3. Напоминаем, что это:

С точки зрения стороннего наблюдателя, эта деталь важна или это только внутренняя забота? Можно ли описать влияние этой внутренней детали, используя только общедоступный API компонента?

Когда я пишу тесты компонентов, я тестирую только общедоступный API компонента (включая побочные эффекты, которые API оказывает на приложение), где это возможно. Точная компоновка этого компонента не зависит от общедоступного API этого компонента; это проблема механизма CSS. Из-за этого это ограничение не соответствует правилу №3. Поскольку это не соответствует правилу №1 и правилу №3, мы не будем тестировать это ограничение, даже если оно проверяет, что TopOverlay получает опору, что обычно важно.

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

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

  • Всегда отображается div.
  • Рендеринг div содержит все остальное, что рендерится.
  • Всегда отображается ClockDisplay.
  • Рендеринг ClockDisplay не получает никаких реквизитов.
  • Всегда отображается SlideToUnlock.
  • Когда переданное свойство onUnlocked определено, обработанное SlideToUnlock получает значение этого свойства в качестве своего onSlide свойства.
  • Когда переданное свойство onUnlocked равно undefined, свойство onSlide рендеринга SlideToUnlock также должно быть установлено на undefined.
  • Если передано свойство wallpaperPath, самый внешний оборачивающий div, который визуализирует компонент, должен иметь свойство background-image css во встроенных стилях, установленное на любое значение wallpaperPath, заключенное в url(...).
  • Если пропущено userInfoMessage, должно быть отрисовано TopOverlay.
  • Если передается свойство userInfoMessage, его значение должно быть передано как дочерние элементы обработанному TopOverlay.
  • Если свойство userInfoMessage не передано, no TopOverlay не должно отображаться.

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

Настройка тестового шаблона

Давайте приступим к созданию теста для этого компонента. Я буду использовать Jest с ферментом в своих тестах. Jest отлично работает с React, а также является средством запуска тестов, включенным в приложения, созданные с помощью create-react-app, так что вы, возможно, уже настроены на его использование. Enzyme - это зрелая библиотека тестирования React, которая работает как в узле, так и в браузере.

Несмотря на то, что я использую Jest и фермент в своих тестах, вы можете применить эти концепции практически к любой тестовой конфигурации.

Это много шаблонов. Позвольте мне объяснить, что я здесь создал:

  • Я создаю let привязки для props и mountedLockScreen, чтобы эти переменные были доступны для всех внутри функции describe.
  • Я создаю lockScreen функцию, доступную где угодно внутри функции describe, которая использует переменную mountedLockScreen либо для mount a LockScreen с текущим props, либо для возврата уже смонтированной. Эта функция возвращает фермент ReactWrapper. Мы будем использовать его во всех тестах.
  • Я установил beforeEach, который сбрасывает переменные props и mountedLockScreen перед каждым тестом. В противном случае состояние из одного теста просочится в другой. Установив здесь mountedLockScreen на undefined, при запуске следующего теста, если он вызывает lockScreen, новый LockScreen будет смонтирован с текущим props.

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

Написание тестов!

Давайте рассмотрим наш список ограничений и добавим тест для каждого. Каждый тест будет написан таким образом, чтобы его можно было вставить в комментарий // All tests will go here в шаблоне.

  • Всегда отображается div.
  • Рендеринг div содержит все остальное, что рендерится.
  • Всегда отображается ClockDisplay.
  • Рендеринг ClockDisplay не получает никаких реквизитов.
  • Всегда отображается SlideToUnlock.

Все ограничения до сих пор были вещами, которые всегда верны, поэтому их тесты было относительно просто написать. Однако остальные ограничения начинаются со слов вроде «Если» и «Когда». Это признаки того, что они условно верны, поэтому мы объединим describe с beforeEach, чтобы проверить их. Здесь пригодится весь тот шаблон тестирования, который мы написали ранее.

  • Когда переданная onUnlocked опора определена, обработанная SlideToUnlock получает значение этой опоры в качестве своего onSlide опоры.
  • Когда переданное свойство onUnlocked равно undefined, свойство отрендеренного SlideToUnlock onSlide также должно быть установлено на undefined.

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

  • Если передано свойство wallpaperPath, самый внешний оборачивающий div, который визуализирует компонент, должен иметь свойство background-image CSS во встроенных стилях, установленное на любое значение wallpaperPath, заключенное в url(...).
  • Если пропущено userInfoMessage, должно быть отрисовано TopOverlay.
  • Если передается свойство userInfoMessage, его значение должно быть передано как дочерние элементы обработанному TopOverlay.
  • Если свойство userInfoMessage не передано, no TopOverlay не должно отображаться.

Это все наши ограничения! Вы можете просмотреть финальный тестовый файл здесь.

«Не моя работа»

Глядя на анимированный gif в начале статьи, вы, возможно, ожидали, что наши тестовые примеры завершатся примерно так:

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

Эта интуиция естественна. С точки зрения приложения, это одни из самых заметных функций.

Однако мы так и не закончили писать тесты для какой-либо из этих функций. Почему? Они не беспокоили LockScreen.

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

Вот удобная шпаргалка, в которой описаны проблемы большинства компонентов React:

  • Что мне делать с полученным реквизитом?
  • Какие компоненты я визуализирую? Что мне передать этим компонентам?
  • Сохраняю ли я что-нибудь в состоянии? Если да, могу ли я аннулировать его при получении нового реквизита? Когда я могу обновить состояние?
  • Что мне делать, если пользователь взаимодействует со мной или дочерний компонент вызывает обратный вызов, который я ему передал?
  • Что-нибудь происходит, когда я нахожусь на коне? Когда меня размонтируют?

Описанные выше функции относятся к SlideToUnlock и ClockDisplay, поэтому тесты для этих функций будут входить в тесты для этих компонентов, а не здесь.

Резюме

Я надеюсь, что эти методы помогут вам написать собственные тесты компонентов React. Обобщить:

  • Сначала найдите контракт на поставку компонентов
  • Решите, какие ограничения стоит проверить, а какие нет.
  • Типы опор не стоит тестировать
  • Встроенные стили обычно не стоит тестировать
  • Компоненты, которые вы визуализируете, и какие реквизиты вы им даете, важно протестировать.
  • Не тестируйте то, что не касается вашего компонента.

Если вы не согласны или нашли этот пост полезным, я буду рад получить известие от вас в твиттере. Давайте все научимся вместе тестировать компоненты React!

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