Как тестировать React-приложения

Тестирование React-приложений вещь, на мой взгляд, довольно утомительная, но необходимая. Иногда она требует даже больше сил, чем реализация самой функциональности. По крайней мере, первое время вы будете тратить немало времени на написание своих первых тестов. Их, к слову, я начал писать не так давно, но даже этих нескольких месяцев хватило, чтобы я для себя определил оптимальную стратегию. И в этом материале я попытаюсь рассказать о том, как тестирую React-приложение в связке с React Testing Library.
Существует 3 типа тестов - юнит, интеграционные и e2e-тесты. Я для себя сделал такие выводы: юнит-тесты это тесты отдельного компонента, который не зависит от чего бы то ни было (от Redux, например). Такой компонент принимает пропсы и отображает информацию. Интеграционные тесты подразумевают кейс, когда вы тестируете компонент, внутри которого находятся другие компоненты - то есть тестируется логика работы нескольких компонентов. Вы как бы тестируете связь компонентов друг с другом. Также под интеграционными тестами я понимаю компонент, который зависит от Redux или любого другого стора (store), то есть он завязан на общем стейте приложения и может влиять на него.
Теперь немного о том, как я тестирую приложение. До настоящего момента у меня были вопросы относительно того, каким тестам отдавать предпочтение. Многие любят юниты, а кто-то интеграционные. Я остановил выбор на обоих типах. Если мне необходимо использовать компонент где-то еще, я покрываю его тестами. При этом внутри компонента могут быть и другие - самый главный вопрос, это используется ли компонент где-то ещё.
И именно поэтому, на мой взгляд, важно покрывать юнит тестами все компоненты дизайн системы: кнопки, селекты, табы и так далее.
Теперь немного об интеграционных тестах. Именно их я в основном использую для тестирования логики приложения, потому что это удобно. Давайте рассмотрим небольшой пример. Представим, что у вас есть список ваших друзей на странице.
Можно сначала написать тесты для компонента Friend - проверить, отображает ли он корректно все филды и выполняются ли нормально все условия во время рендеринга. Если у элемента списка есть кнопка "Убрать из друзей", мы можем в юнит тесте компонента замокать (от сл. mock) хэндлер кнопки и проверить, вызвался ли он при нажатии. Но в этом, по факту, нет никакого смысла, потому что вам всё равно придется тестировать FriendsList и проверять как работает логика отправки запроса на бэк и реакция на запрос на нашей стороне.
Поэтому в случае с логикой страниц, где происходят постоянные запросы на бэкенд, на мой взгляд, лучше тестировать именно логику компонента, который отвечает за отправку запросов на бэкенд - всё, что ниже этого компонента тестировать смысла, на мой взгляд, нет. Чуть подробнее о такой логике написания тестов можно почитать в этом материале.
Итак, с логикой того, какие компоненты выбирать для тестирования примерно разобрались.
MWS JS - библиотека для тестирования API-запросов
Есть два варианта тестирования запросов на бэк:
- мок-объект функции отправки запроса, например, axios.get или fetch.
- Мок API
Мне понравился второй вариант, так как он более понятный и более близок к реальной работе приложения. Для мока API я использую Mock Service Worker. С помощью него можно легко создать фейковые эндпоинты как для автоматических тестов, так и для тестирования непосредственно в браузере.
Теперь немного о стейте Redux. Я использую во всех своих компонентах общих baseState, где храню базовый стейт приложения. Его я прокидываю в кастомный render метод React Testing Library.
Использую его так:
В baseState может храниться, например, id активной подписки, которое может пригодится в каком-нибудь компоненте, данные аккаунта. Там можно хранить любую информацию, доступ к которой необходим всем страницам во время запросов на бэкенд.
Если бы наш компонент FriendsList из примера в начале статьи делал запросы на бэкенд через thunk и хранил весь стейт в редаксе:
Тест для такого компонента выглядел ты следующим образом (Удаляем Дэниела из друзей).
Такой тест будет работать, если мы не забыли замокать наше API. В конкретном случае baseState нам не понадобился. Но если бы нам для удаления друга понадобилось дополнительно отправлять запрос на другой эндпоинт с именем текущего юзера, baseState в thunk бы нам пригодился.
Асинхронность React Testing Library (RTL)
Когда вы делаете запросы на бэкенд, после этих действий обязательно необходимо использовать await findBy, чтобы RTL ждал окончания запроса. Для обычного поиска элементов используем getBy или queryBy. Также вместо findBy можно использовать await waitFor(() => {}); При этом waitFor должен возвращать результат. Если вы ожидаете, что элемент удалится из DOM после запроса, необходимо использовать queryBy в связке с waitFor, потому что findBy кидает ошибку, если не находит элемент. Да, всё это выглядит запутано, и так оно и есть. Асинхронная логика в RTL, на мой взгляд, запутана и на первых порах вы можете долго залипать в код, не понимая, почему RTL не ждёт выполнения запроса.
Для симуляции нажатия по кнопкам я использую userEvent. Это отдельная библиотека, являющаяся частью RTL. Она мне показалась более близкой к реальному использованию приложения по логике работы.
Наверное, этого будет достаточно, чтобы вы примерно понимали, как работает тестирование приложений в React. Если есть вопросы, делитесь ими в Twitter.