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

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

Чтобы увидеть больше тестовых примеров, вы можете взглянуть на мою Реализацию React TodoMVC или Реализация React Hooks TodoMVC.

1. Типы

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

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

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

2. Тестовый прогон

Для новых проектов проще всего добавить тестирование в проект с помощью инструмента Создать приложение React. При генерации проекта (npx create-react-app myapp) нужно включить тестирование. Модульные / интеграционные тесты могут быть написаны в каталоге src с суффиксом *.spec.js или *.test.js. Приложение Create React использует среду тестирования Jest для запуска этих файлов. Jest - это не просто средство запуска тестов, в отличие от Mocha, он также включает в себя библиотеку утверждений.

3. Единый блок

Пока все хорошо, но мы еще не написали никаких тестов. Напишем наш первый модульный тест!

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

Первая задача (аранжировка) - перевести цель (в данном случае функцию) в тестируемое состояние. Это может означать импорт функции, создание экземпляра объекта и установку его параметров. Вторая задача - выполнить эту функцию / метод (действие). После того, как функция вернула результат, мы делаем утверждения для результата.

Jest дает нам две функции: describe и it. С помощью функции describe мы можем организовать наши тестовые примеры вокруг модулей: модуль может быть классом, функцией, компонентом и т. Д. Функция it предназначена для написания фактического тестового примера.

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

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

4. Компонентный дисплей

Следующим шагом будет написание интеграционного теста для компонента. Почему это интеграционный тест? Потому что мы больше не тестируем только код Javascript, а скорее взаимодействие между DOM и соответствующей логикой компонентов.

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

Первый компонент, который мы тестируем, - это тот, который отображает свое состояние и изменяет его, если мы нажимаем кнопку.

Чтобы отобразить компонент в тесте, мы можем использовать рекомендованный метод render Библиотеки тестирования React. Для рендеринга функции render необходим действующий элемент JSX. Аргумент возврата - это объект, содержащий селекторы для визуализированного HTML. В этом примере мы используем метод getByTestId, который извлекает элемент HTML по его атрибуту data-testid. В нем гораздо больше методов получения и запроса, вы можете найти их в документации.

В утверждениях мы можем использовать методы из плагина Jest Dom, который расширяет стандартную коллекцию утверждений Jests, упрощая тестирование HTML. Все методы утверждения HTML ожидают в качестве входных данных узел HTML и обращаются к его собственным свойствам.

5. Взаимодействие компонентов

Мы протестировали то, что мы видим в модели DOM, но пока не взаимодействовали с компонентом. Мы можем взаимодействовать с компонентом через DOM и наблюдать за изменениями через его содержимое. Мы можем вызвать событие щелчка на кнопке и наблюдать за отображаемым текстом.

Нам нужен элемент DOM, в котором может быть запущено событие. Получатели, возвращенные методом render, возвращают этот элемент. Объект fireEvent может запускать желаемые события через свои методы элемента. Мы можем проверить результат события, как и раньше, наблюдая за текстовым содержимым.

6. Взаимодействие родителей и детей

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

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

В тесте мы должны предоставить props в качестве входных данных и проверить, вызывает ли компонент опору функции onModify.

Мы передаем компоненту свойство info и свойство onModify через JSX. Когда мы запускаем событие щелчка на кнопке, вызывается метод onModify, который изменяет переменную callArgument своим аргументом. Утверждение в конце проверяет callArgument, было ли оно изменено опорой функции дочерних компонентов.

7. Интеграция магазина

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

Хранилище имеет единственное состояние, такое же, как то, что мы видели в компоненте. Мы можем изменить состояние с помощью действия onModify, которое передает входной параметр редуктору и изменяет состояние.

Давайте построим магазин и напишем интеграционный тест. Таким образом, мы можем проверить, работают ли методы вместе, вместо того, чтобы вызывать ошибки.

Мы можем изменить хранилище с помощью метода dispatch. Параметром метода должно быть действие со свойством type и payload. Мы всегда можем проверить текущее состояние с помощью метода getState.

При использовании магазина с компонентом мы должны передать экземпляр магазина в качестве поставщика функции render.

8. Маршрутизация

Самый простой способ показать, как тестировать маршрутизацию внутри приложения React, - создать компонент, отображающий текущий маршрут.

Компонент Footer обернут методом withRouter, который добавляет к компоненту дополнительные props. Нам нужен другой компонент (App), который обертывает Footer и определяет маршруты. В тесте мы можем утверждать содержимое элемента Footer.

Мы добавили наш компонент как универсальный маршрут, не определяя путь в элементе Route. Внутри теста не рекомендуется изменять API истории браузеров, вместо этого мы можем создать реализацию в памяти и передать ее с помощью свойства history в компоненте Router.

9. HTTP-запросы.

Мутация начального состояния часто происходит после HTTP-запроса. Хотя соблазнительно позволить этому запросу достичь места назначения в тесте, это также сделало бы тест хрупким и зависимым от внешнего мира. Чтобы этого избежать, мы можем изменить реализацию запроса во время выполнения, что называется имитацией. Для этого мы будем использовать встроенные в Jest возможности имитации.

У нас есть функция: входной параметр сначала отправляется через запрос POST, а затем результат передается методу commit. Код становится асинхронным и получает Axios как внешнюю зависимость. Внешняя зависимость будет той, которую мы должны изменить (имитировать) перед запуском теста.

Мы создаем поддельную реализацию для метода commit с jest.fn и меняем исходную реализацию axios.post. Эти фальшивые реализации фиксируют переданные им аргументы и могут отвечать тем, что мы им прикажем вернуть (mockImplementation). Метод commit возвращает пустое значение, потому что мы его не указали. axios.post вернет Promise, который преобразуется в объект со свойством body.

Тестовая функция становится асинхронной, если перед ней добавляется модификатор async: Jest может обнаружить и дождаться завершения асинхронной функции. Внутри функции мы ждем, пока метод onModify завершится с await, а затем делаем утверждение, был ли вызван поддельный метод commit с параметром, возвращенным из пост-вызова.

10. Браузер

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

Create React App не имеет встроенного решения для тестирования E2E, мы должны организовать его вручную: запустить приложение и запустить тесты Cypress в браузере, а затем закрыть приложение. Это означает установку Cypress для запуска тестов и библиотеки start-server-and-test для запуска сервера. Если вы хотите запустить тесты Cypress в автономном режиме, вы должны добавить в команду флаг - headless.

Организация тестов такая же, как и в случае модульных тестов: describe обозначает группировку, it обозначает выполнение тестов. У нас есть глобальная переменная cy, которая представляет бегуна Cypress. Мы можем синхронно командовать бегуном о том, что делать в браузере.

После посещения главной страницы (visit) мы можем получить доступ к отображаемому HTML через селекторы CSS. Мы можем утверждать содержимое элемента с помощью contains. Взаимодействия работают одинаково: сначала выберите элемент (get), а затем выполните взаимодействие (click). В конце теста мы проверяем, изменился ли контент.

Резюме

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

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