Как тестировать компоненты React.js с помощью Jest и Testing Library. Тестовые события, асинхронное поведение, имитационные вызовы API и многое другое.

Тестирование является важным этапом в жизненном цикле разработки программного обеспечения. Иногда мы «забываем» об этом. А в других случаях мы просто предполагаем, что это внутренняя задача.

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

Итак, давайте кратко рассмотрим, как вы будете тестировать свои компоненты React.

Инструменты, которые мы будем использовать

Как вы можете знать или не знать, существует МНОЖЕСТВО инструментов для использования, но я буду придерживаться того, что предоставляется по умолчанию, что означает: я буду использовать JEST + Библиотека тестирования.

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

Настройка JEST + библиотека тестирования

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

Обе библиотеки рекомендуются командой React для тестирования, а это означает, что такие инструменты, как create-react-app, уже устанавливают их из коробки.

И если вы используете, например, Next.js, вот пример проекта, показывающего вам, как настроить обе эти библиотеки с фреймворком.

В этом уроке я буду использовать проект, созданный с помощью create-react-app, что означает, что запускать тесты так же просто, как использовать команду npm test, а создавать новые — так же просто, как создавать файл с именем, заканчивающимся на test.js. Итак, если у вас есть компонент с именем Counter.js, рекомендуется создать рядом с ним файл с именем Counter.test.js.

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

Тестирование вашего первого компонента

Давайте сделаем наш первый тест простым. Я создам простой компонент Greeter, который говорит вам «привет» двумя разными способами, в зависимости от того, передаете ли вы свойство name или нет:

Как видите, у нас есть два потенциальных выхода этого компонента, теперь давайте создадим и просмотрим тест для них:

Здесь можно увидеть несколько интересных вещей:

  1. Я импортирую элементы render и screen из библиотеки тестирования. Функция render преобразует наш компонент в поддельный DOM, который мы сможем запросить позже. И объект screen будет тем интерфейсом запроса, обратите внимание, как я могу получить визуализированный элемент с помощью метода getByText.
  2. Я вызываю функцию test дважды, хотя никогда не импортировал ее откуда-либо. То же самое касается функции expect. Это помощники Jest, которые Jest добавит автоматически, поэтому мне (и вам) не нужно об этом думать.
  3. Я тестирую этот конкретный компонент путем рендеринга с реквизитом или без него, а затем пытаюсь захватить визуализированный HTML-элемент, ища внутри него ожидаемый текст. Затем я устанавливаю ожидание, что захваченный элемент должен быть внутри документа (то есть он должен быть отрисован). Это может быть излишним, поскольку, если рендеринг пойдет не так, как планировалось, переменная pElement будет равна null, и я мог просто ожидать, что она не будет нулевой. Так что я мог бы написать что-то вроде expect(pElement).not.toBeNull(), и это сработало бы так же хорошо.

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

Добавление интерактивности: срабатывание событий

Предыдущий тест был приятным и простым, но мы не часто имеем дело с такими компонентами в нашем коде. Мы?

Обычно компоненты обладают некоторой интерактивностью, которую нам нужно протестировать, поэтому давайте создадим компонент с несколькими кнопками. А не ___ ли нам?

Представляю вам компонент Counter:

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

У меня есть две кнопки, которые меня сейчас волнуют: + и -.

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

Я снова импортирую элементы render и screen из библиотеки тестирования, но на этот раз я также импортирую объект fireEvent. Этот объект позволит мне взаимодействовать с поддельным DOM, о котором я упоминал ранее.

Если вы обратите внимание, оба теста практически одинаковы:

  1. Я визуализирую элемент
  2. Я нажимаю на соответствующую кнопку
  3. Я получаю вывод и проверяю, имеет ли полученный HTML правильное значение внутри.

Интересные моменты об этих шагах:

  1. Я получаю кнопки, используя текст внутри них. Я не использую идентификатор или класс, потому что а)я знаю, что текст моей кнопки уникален, иб) библиотека тестирования не предоставляет никаких методов запрашивать DOM через имена классов или идентификаторы, потому что они могут варьироваться, не затрагивая фактический HTML, так зачем мне привязывать свои тесты к этим параметрам? Я не должен, и ты тоже не должен, поэтому они даже не дают тебе попробовать.
  2. Однако я получаю результирующий элемент, используя метод getByTestId. «Идентификатор теста» — это специальный атрибут данных, который будет искать библиотека. Он работает как метаданные тестирования или, другими словами, это данные, которые вы можете использовать для идентификации своих элементов только в своих тестах, поэтому они не повлияют на вашу бизнес-логику и не привяжут ваши тесты к что-то, что может измениться в будущем. Обратите внимание, что в коде компонента у меня есть свойство data-testid для элемента, отображающего переменную value.
  3. Наконец, обратите внимание, что я обращаюсь к свойству outerHTML захваченного объекта внутри вызова expect. Это связано с тем, что возвращаемое значение из запроса (вызов getByTestId ) не является строкой, фактически это объект HTMLElement, поэтому я не могу использовать метод сопоставления строк, который использую.

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

Проверка асинхронного поведения

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

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

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

Давайте посмотрим пример.

Вернитесь к коду компонента счетчика и обратите внимание на последнюю кнопку: кнопку «Показать». Всякий раз, когда я нажимаю на нее, обработчик onClick будет вызывать setTimeout с задержкой в ​​500 мс. После этой задержки код обновит переменную состояния, и на экране появится новый элемент p.

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

Функция expect будет вызвана сразу после клика, но время ожидания еще не истекло, поэтому тест не пройдет.

Теперь я знаю, что вы можете подумать: «А что, если я уменьшу время ожидания?».

К сожалению, это ничего не изменит, вы можете изменить тайм-аут вызова setTimeout на 0, мне все равно, и результат будет тот же, потому что обратный вызов будет добавлен в очередь, а не вызван немедленно.

Итак, как нам решить эту проблему? С помощником waitFor!

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

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

Сколько стоит «некоторое время»? По умолчанию функция waitFor будет ждать 1 секунду (мне этого было достаточно, так как я установил задержку 500 мс). Однако, если у вас есть более длительные тайм-ауты в ваших тестах, вы можете перезаписать значение по умолчанию следующим образом: waitFor( () => {...}, {timeout: <your number of milliseconds here>})

Другими словами, есть второй параметр для настройки поведения функции.

Кроме того, если у вас большие задержки в тестах, вам следует перепроверить их правильность. Значение: ваши тесты должны тестировать только ваши компоненты, а не внешние ресурсы, с которыми они работают. Если вы тестируете компонент, который отправляет запрос к внешнему API, этот запрос следует смоделировать, чтобы удалить зависимость между вашим тестом и статусом этого API (он может быть недоступен, и это приведет к сбою ваших тестов, заставляя вас думать что-то не так с вашим компонентом).

На самом деле, давайте быстро рассмотрим компонент, который делает запрос fetch, и как его имитировать.

Имитация внешних вызовов API внутри тестов вашего компонента

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

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

Таким образом, тест для такого компонента потребует от нас установки пакета jest-fetch-mock. Установив его, мы можем имитировать функцию fetch внутри наших тестов.

Итак, мы можем решить, когда вызовы API успешны, а когда нет, вот так:

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

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

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

На этом мы завершаем это руководство по тестированию, основанное на примерах! Вы узнали что-нибудь новое? Используете ли вы другие библиотеки для тестирования вашего компонента React? Поделитесь ими в комментариях, чтобы другие могли узнать о них!

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

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

Подробнее

Создавайте приложения, страницы, пользовательский опыт и пользовательские интерфейсы как автономные компоненты. Используйте их, чтобы быстрее создавать новые приложения и возможности. Используйте любой фреймворк и инструмент в своем рабочем процессе. Делитесь, повторно используйте и сотрудничайте, чтобы строить вместе.

Помогите своей команде:

Микроинтерфейсы

Дизайн-системы

Совместное использование кода и повторное использование

Монорепо

Узнать больше