(за основу взята create react app сборка)
Мы уже протестировали логику (экшены и редьюсеры для нашего списка новостей), теперь продолжим тестирование со стороны компонента.
Подопытный — <NewsContainer />
NewsContainer.js
Стандартный redux-контейнер, на всякий случай добавил комментариев.
Что будем тестировать:
NewsList
компонент, если в props
есть новости (есть data
);Preloader
— если в props isLoading:true
;Для тестирования компонентов, нам потребуется библиотека enzyme и адаптер. Рассматривайте адаптер, просто как набор дополнительного кода для конкретной версии реакта. В туториале речь идет про 16ю версию, значит и адаптер, тоже будет 16й версии.
Далее необходимо создать 2 конфигурационных файла в директории src/
. Имена файлов должны совпадать, если вы стартовали проект с create-react-app.
setupTests.js
tempPolyfills.js
Стандартные describe и it + ожидание.
NewsContainer.spec.js
Получим ошибку: нет ни одного теста.
Ключевой момент тестов компонентов — мы можем находить элементы из shallow (функция из enzyme) представления компонента. То есть, можно сказать, что если компонент рисует один абзац <p>123</p>
, мы можем выстроить ожидание следующим образом:
Принципы тестирования сохраняются — мы ожидаем что-то. В случае с тестами компонентов, мы часто будем ожидать, что какой-то элемент (или компонент) представлен на странице.
В тесте выше, мы использовали ассерт toHaveLength
, поэтому ожидание можно перевести так:
ожидаю, что в случае отрисовки
myComponent
будет найден тэг<p>
и количество таких вхождений равно 1.
Синтаксис .find
метода очень похож на jQuery. То есть, мы можем искать по селектору, по id и тд тп. Приятный бонус, с помощью .find
мы можем искать по имени компонента.
Документация по .find (EN).
Сформируем тест для нашего компонента NewsContainer в случае «первичной отрисовки». Взглянем на его render:
Получается требование для теста следующее:
NewsList
длина равна 0 (компонент не отрисовывается)<p>
длина равна 0 (абзацы не отрисовываются)После постановки задачи, тест уже не кажется сложным:
NewsContainer.spec.js
Получается, остальные варианты отрисовки в зависимости от свойств будут такими же. Свойство изменилось — ожидание тоже. Передать свойства — не проблема:
Предлагаю сразу добавить тесты, для случая:
<p>Loading...</p>
NewsList
компонентДля поиска текста в абзаце, можно воспользоваться функцией из enzyme — text(). Для прелоадера, выглядеть будет так:
Каждый тест сгруппируем в свой describe
.
Так же, для всех наших тестов, нужно описать корректные props. Сделать это легче простого, так как props — это просто объект.
Итого:
NewsContainer.spec.js
Здесь количество it — зашкаливает, да и код очень однотипный (копи-паста) Посмотрите внимательно. Общий принцип должен быть понятен.
Конечно, данную копи-пасту можно отрефакторить. Но прежде, я предлагаю взглянуть на возможность тестирования «снапшотами».
Избавиться от достаточно скучного написания it
‘ов, нам помогут снапшоты.
Снапшот — это некое представление компонента, то есть снимок. При первом прогоне тестов, в которых есть вызов toMatchSnapshot
— создается снапшот. Затем, на каждый новый вызов происходит сравнение двух снимков. Если нет разницы — тест зеленый. Если разница есть — тест сломается, но вам будет предложено обновить снапшот, если вы желаете, так как вы могли в процессе работы изменить компонент.
Такой блок it, вы можете добавить в каждый тест.
Если заменить в NewsContainer «Loading…» на «Загружаю…»:
NewsContainer.js
То при перезапуске тестов (а это случится автоматически, если вы не прерывали работу jest) увидим:
Написано: 1 снапшот-тест провалился, проверьте свой код или обновите snapshot нажав на u
.
Таким образом есть выбор: проверить код или обновить снапшот.
Что нам дает тестирование снапшотов?
1) можем избавиться от большого количества точечных проверок (которые мы расписали в it), оставив только какие-то ключевые моменты.
2) возможность контролировать изменения UI с помощью тестов в терминале, а это быстро.
Окей, предлагаю вам вернуть текст Loading...
обратно и подчистить файл с тестами до следующего состояния:
NewsContainer.spec.js
Моя рекомендация: оставляем только снапшот на «начальную загрузку», а во всех осатльных случаях, оставляем снапшот + то, что должно отрисоваться по условию. Следовать данному подходу не обязательно, достаточно будет и снапшотов.
Набор тест для NewsContainer почти готов. Осталось убедиться, что в componentDidMount вызывается функция, которая пришла в компонент из props (из redux).
Для теста, нам не важно, что внутри функции. Мы хотим проверить, что функция была вызвана. Один раз. Только и всего.
Для этого, jest предоставляет удобный функционал создания заглушки («мока») — jest.fn()
.
В describe с News container initial добавить новый тест. Для этого:
toHaveBeenCalledTimes(1)
(то есть, было вызвано один раз)NewsContainer.spec.js
Все тесты зеленые:
По традиции, так как мы нарушаем TDD и пишем тесты на уже готовый код, я предлагаю сломать наш тест, удалив реальный вызов функции из компонента NewsContainer.
Получим:
Тест работает. Можно вернуть вызов функции на место.
На этом, компонент NewsContainer покрыт тестами достаточно.
Покроем тестами <NewsList />
.
NewsList.js
Что будем тестировать?
NewsList.spec.js
Тест достаточно скучный и понятный. Все что используется, мы уже обсуждали. К find добавилось .first(). Эта функция берет первый элемент из найденной коллекции (как вы помните, у нас в props две новости, значит и в коллекции в тесте будет 2 элемента).
Чтобы взять вторую новость, можно воспользоваться функцией .at(), которая ожидает на index.
Когда в результате поиска возвращается коллекция элементов, необходимо обращаться по индексу к конкретному элементу, если того требует тест. В противном случае получим такую ошибку:
(закомментируйте вызов first()
)
NewsList.js
В терминале:
Задание: Напишите тесты для проверки текста и заголовка второй новости в отдельном describe блоке.
В теории, тесты изменения стэйта и клик не отличаются от вышеописанного. Снова заранее известное ожидание, снова проверка на соответствие.
В данном разделе будем покрывать тестами стандартную форму:
Login.js
Помимо формы, в компоненте присутствует параграф с ошибкой (если она есть в props), поэтому в список добавим еще один тест:
Заготовка для тестов + тест снапшотом:
Login.spec.js
Так как я прогоняю этот тест в рамках создания статьи уже не в первый раз, у меня нет надписи: добавлен новый снапшот.
Чтобы вытащить state из компонента, у shallow-представления, можно вызвать одноименную функцию .state()
Добавьте в describe после теста снапшотом еще один тест:
Надеюсь, вам еще не наскучило. Как обычно: ожидаем, что наш стейт компонента будет равен заранее приготовленному стейту в переменной initialState.
В реальности: пользователь вводит что-то в input, случается событие onChange, по которому выполняется функция:
Обновим основы: если мы хотим считать что-то из атрибута data-*, то мы должны использовать dataset свойство у объекта event.
Enzyme предоставляет нам функцию simulate. С помощью нее мы можем создавать события: клики, onChange и т.д.
Однако, пример из-за использования data-* может показаться чуть-чуть запутанным, добавим следующий код:
Заострить внимание хотелось бы на комментариях 1,2,3. Вспомните, у нас в handleChange функции в реальном компоненте такая запись:
const fieldName = e.currentTarget.dataset.fieldName
То есть, мы вынимаем значение из e.currentTarget(1).dataset(2).fieldName(3) (цифрами помечены соответствия между реальным кодом и объектом в simulate функции).
Чтобы закрепить работу с simulate, давайте изменим поведение работы реального компонента:
Интересуют строчки 26, 33 и 4. Что мы сделали? Просто стали хранить строку ’email’ или ‘password’ в атрибуте id
. Значит симуляция в тесте у нас тоже должна измениться. Пока что мы имеем сломанный снапшот и сломанный тест на изменение email.
[1] — метод simulate подразумевает запуск на node (узле). 0 узлов найдено.
[2] — мы ожидаем, что у нас значение будет max@test.com, а получаем пустую строку.
Починить не сложно, нужно лишь изменить симуляцию в beforeEach
:
С id выглядит уже просто и знакомо, не так ли? Какой из вариантов оставить — дело ваше, я продолжу работать с data-* атрибутом.
Задание: написать тест на изменение пароля. Подсказка: он будет в своем describe со своим beforeEach.
Так как у формы есть onSubmit
и у кнопки есть type='submit'
, значит форму можно отправить двумя способами:
Значит два теста с симуляцией. В первом тесте симулируем submit событие, во втором — click. В обоих тестах ждем, что props.logIn
будет вызвана 1 раз.
Задача ясна?
Если не передать в event объект с методом preventDefault, то тест сломается, так как jest не сможет вызвать e.preventDefault()
, который имеется в реальном коде. Я намекаю на то, чтобы вы не забывали: да, мы вызываем реальные методы из наших компонентов.
Все неплохо, но есть проблема. Каждый блок тестов, или каждый тест, или каждый сценарий (как вы задумаете) должен очищать после себя общие данные. То есть, если вы этого явно не хотите, предыдущий тест/блок тестов не должен влиять на последующие.
Добавьте простенький it перед закрывающей })
общего describe (то есть в конец всех ваших тестов):
[1] — несмотря на то, что тест добавлен в конец, так как он лежит на один уровень describe выше, чем вложенные тесты формы, он будет показан до них (хотя выполнится позже)
[2] — то, о чем я писал. Забыли прибраться. Если ниже будут тесты, которые не ожидают, что state изменен, можно долго искать ошибку.
Мы уже использовали хук — beforeEach. В jest хуков четыре:
По ссылке выше в документации, есть порядок срабатывания хуков для вложенных друг в друга describe.
Нам же понадобится:
Итоговый код для тестирования формы логина на данный момент:
Login.spec.js
Как видим из console.log
— форма очистилась.
Последний it тест нужно удалить.
Исходный код на текущий момент
В нашей форме есть несколько проблем с кнопкой отправки.
Условие для валидации на данный момент простое: оба поля, не могут быть пустыми.
Так как TDD подразумевает, сначала пишем тест, а затем пишем рабочий код, то придется порассуждать.
Нам необходима функция-валидатор, которая будет выполнять следующее:
Далее, результат этой функции мы будем передавать в атрибут disabled для нашей кнопки.
Будем превращать слова в код, добавляя в Login.spec.js новые тесты. Создадим новый describe блок в родительском блоке.
[1] Для поиска атрибута disabled воспользуемся методом prop(‘ключ’), где ключ — необходимое нам свойство. Нам достаточно, чтобы атрибут disabled был у кнопки, то есть его значение было бы true. Для такой проверки, подойдет ассерт toBeTruthy.
Итого, наш тест выглядит так:
Конечно, наш тест сломался и это хо-ро-шо. Напоминаю, мы работаем по TDD — мы написали тест, а код который удовлетворяет этому тесту еще нет. Нужно изменить код в компоненте.
Login.js
Ок, теперь все тесты снова зеленые (не забудьте обновить снапшот, так как мы изменили отрисовку компонента).
На всякий случай подробности: this.validate()
будет вызываться в render на каждое изменение инпутов, так как изменение стейта вызывает render. Что нам и нужно, собственно.
Если validate вернул false, мы это значение инвертируем и наша кнопка получает атрибут disabled=true
и наоборот.
Конечно, такая валидация нам не очень интересна, предлагаю прокачать форму.
Новые условия валидации:
Для пароля, на домашнее задание будет написать регулярку, под необходимое вам условие.
Первый момент — организационный. Функции для проверки пароля/почты вынесем в директорию helpers.
helpers/inputs.js
Затем, будем вызывать хелперы внутри validate метода
Изменим наш компонент… Стоп! Мы же сначала должны написать тесты.
Благо, у нас уже есть необходимые знания. Что будет в тестах:
Login.spec.js
Конечно, они упадут:
Обновим код компонента:
Login.js
Зеленым зелено! А главное, форму можно не проверять в браузере, так как наши тесты гарантируют ее работоспособность!
Что ж, осталось только сделать тест, что если данные введены нормально, то у кнопки нет атрибута disabled, для этого понадобится ассерт — toBeFalsy.
Готово. Исходный код. Репозиторий был обновлен, так как npm/jest/кто-то еще сломался (ссылка на баг)
Сегодня будем использовать parcel и IntelliJ IDEA Community Edition. Все инструменты бесплатные. Инициализация elm проекта…
На данном вебинаре мы знакомились с языком Elm проводя параллели между Elm и Redux, поэтому…
Richard Feldman рассказывает как масштабировать Elm приложение без боли. Показаны техники: extended records, подход narrow…
В данной заметке вы найдете конспект видео по Elm, которые я посмотрел в ноябре 2019.…
Итоги года 2019 // Max Frontend Покажи мне свой гитхаб, и я скажу работал ли…
Почему стоит изучать Elm? Потому что это интересный вызов, редкие (но вкусные) вакансии и хороший…
View Comments
Давно слежу за твоей работой - дико круто! написано просто, ясно, доходчиво, со всеми возможными примерами. учил реакт по первой версии твоего учебника - более простого объяснения не видел)
еще раз спасибо за работу! :idea:
крутой урок! Но будь добр, исправь setupTets.js на setupTests.js. 2 часа потратил на такую фигню
Спасибо, исправил! :twisted:
В тесте, в котором тестируется отправка формы по идее ошибка: при клике на кнопку количество вызовов функции mockLogin ожидается равным 1, а при нажатии на кнопку мы получается отправляем форму, а соответственно вызываем mockLogin второй раз.