Unit тестирование redux экшенов и редьюсеров

Запись вебинара


(примеры построены на основе create-react-app, в котором jest и запуск тестов уже настроен из коробки)

Unit тестирование позволяет покрыть тестом функцию. Пример:

Мы протестировали функцию «приветствие» (introduce). Причем, в тесте (внутри конструкции it) мы взяли уже существующую функцию и вызвали ее с аргументом «Max Frontend».

Три технических момента из реального кода на Jest:

  • it — блок с юнит-тестом. Должен содержать expect, иначе тест будет успешным всегда.
  • expect — функция «ожидаю». Ключевая функция проверки.
  • toEqual — функция-ассерт. Ее задача указать, что мы ожидаем. В тесте выше, мы ожидаем «что результат работы функции intoduce('Max Frontend') будет эквивалентен строке 'Привет, мой youtube канал Max Frontend'«.

Что такое редьюсер в redux приложении? Функция! То есть тестирование редьюсеров настолько простое, что для этого даже не нужно учить что-то еще о юнит-тестировании. Главное понять суть такого тестирования: мы вызываем функцию и ожидаем, что результат будет таким, каким мы его описали в тесте.

Здесь не хватает только слова describe, которое вы часто можете увидеть в тестах для группировки.

Тестирование редьюсеров

По TDD (test driven development, разработка через тестирование), мы должны сначала написать тест, а потом код, но для обучения будет удобно распутать клубок с другой стороны: код уже есть, покроем тестами.

Представим, что у нас есть приложение, которое загружает новости. Стандартный расклад:

  • есть три типа экшенов (request — загружаю, success — загрузил, failure — ошибка);
  • есть три поля в редьюсере:
    • isLoading — флаг, указывающий загрузка в процессе или нет,
    • data — массив с новостями,
    • errorMsg — сообщение об ошибке.

actionTypes.js

reducer.js

Action types (типы экшенов, то есть строка в константе) хранятся в отдельном файле, для удобства подключения их в редьюсере и в экшенах.

Так как редьюсер реагирует на определенный тип экшена, то нам вообще нет необходимости знать что либо об action creator’ax (функциях создателях экшенов), нам нужен только правильный action type.

Представим первый тест для ситуации «новости загружаются» — t.NEWS_GET_REQUEST:

Псевдо-код:

Напишем:

reducer.spec.js (spec = файл с тестом, один из подходов к наименованию)

В первых строчках мы импортировали необходимое. Далее с помощью describe объединили все юнит тесты (их будет три) редьюсера новостей в группу, чтобы в терминале они отображались в красивой структуре.

Запускайте в терминале: yarn test

news_get_request. unit test in terminal

После выполнения команды yarn test, ваши тесты будут запущены в watch-mode (смотрящий режим). То есть при изменении файлов в проекте (в том числе и кода тестов) — тесты будут перезапускаться.

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

Посмотрим на сам тест:

Финиш.


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

  • isLoading флаг перевести в состояние false
  • в поле data положить массив с новостями [1]

[1] — нам не важно какие данные мы положим в data. Нам важно, чтобы указанное в экшене поле, корректно обрабатывалось в редьюсере. Если вы посмотрите на код редьюсера, то увидите, что данные берутся из action.payload.

reducer.js

reducer.spec.js

unit testing part 1, success case in terminal

Финиш? Увы. Есть проблема.

Перейдите в код редьюсера, и в t.NEWS_GET_SUCCESS удалите строку isLoading: false

Тесты «зеленые». А код не рабочий.

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

Проблема нашего теста в том, что он берет неправильное «начальное состояние» для случая t.NEWS_GET_SUCCESS. Это же очевидно, что если мы ожидаем «успешную загрузку», то для такого кейса начальное состояние должно было бы быть с флагом isLoading: true.

Иными словами, в тестах редьюсера, в «начальном состоянии» для каждого случая должно быть состояние на 1 шаг до события.

Исправляем тест:

reducer.spec.js

В терминале видим первый «красный» тест:

news success test broken

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

Мы ожидаем, что флаг isLoading будет false, а у нас (реальный код отработал так) — true.

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


Внимательный читатель может заметить, что наш t.NEWS_GET_REQUEST так же требует доработок.

Посмотрим на реальный код:

Почему же тест проходит успешно? Потому что, в начальном состоянии у нас errorMsg: null, следовательно, никаких проблем.

Как быть? План состоит из одного пункта:

  • мы должны воссоздать ситуацию «за 1 шаг до»

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

Обратите внимание, в it я указал понятное название для теста. Тест в данный момент падает с ошибкой:

тест request после ошибки в терминале

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

Тесты зеленые. Добавим ожидание ошибки null в нашем первом тесте NEWS_GET_REQUEST и переименуем его в NEWS_GET_REQUEST after situation without errorMsg (название экшена + «ситуация без ошибки»)

Итого, файл с тестами будет выглядеть так:

reducer.spec.js

success tests with good naming


Задание

Напишите самостоятельно тест для кейса t.NEWS_GET_FAILURE. Обратите внимание на то, какое поле вы обрабатываете в редьюсере. Такое же поле (с тем же уровнем вложенности) нужно воссоздать в объекте action, в вашем тесте.


Документация по redux предлагает добавить еще один тест, который называется: «должно вернуть начальное состояние».

На этом все, по unit-тестированию редьюсеров. Единственное, не забывайте, что по правилам, сначала пишется тест, он «падает», а затем пишется код, чтобы тест «прошел».


Тестирование экшенов

Тестирование синхронных экшенов

Чтобы не забывать про терминологию, на самом деле мы будем тестировать «создателей действий» (action creators) — функции, которые возвращают объекты, так как экшен — это… ну вы поняли, простой объект. Я повторяю это раз за разом.

Предположим, у нас есть такой action creator:

Это простая функция, а функции мы тестировать уже умеем.

Даже смешно ;) Ну что здесь тестировать?

Ок, еще пример синхронного экшена (экшен-крейтора, если быть занудой):

и тест:

Пожалуй, хватит. Идея ясна:

  • мы импортировали функцию. Вызываем ее в expect и передаем в нее аргументы, если нужно
  • далее пользуемся знакомым нам ассертом toEqual, чтобы сравнить результат работы функции с нашим ожиданием

Тестирование асинхронных экшенов

Что такое тест асинхронного экшен-крейтора? Поставим вопрос еще чуть более «канцелярно»: что делает асинхронный экшен-крейтор (функция) — диспатчит несколько экшенов.

Представим как бы это могло выглядеть в теории (псевдо-код):

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

Однако, перед тестом хотелось бы обсудить еще один момент. Тесты должны производится в изолированной среде, в неком «тестовом окружении». Нам нет необходимости делать реальный асинхронный запрос, так как придется ждать «ответ сети», который мы контролировать не можем: двадцать раз интернет будет хорошим и тест пройдет, а на двадцать первый лаганет соединение и вы будете ждать выполнение теста минуту? Или ответ завершится неудачей (что тоже нужно тестировать), но вы не знаете в каком тесте будет удачно, а в каком нет.

Для решения вышеописанной проблемы мы воспользуемся библиотекой fetch-mock, которая позволит «мокнуть» или «сделать мок», то есть заглушку. Например, мы «мокнем» наш API-адрес, и fetch-mock перехватит реальный запрос.

Итого, перед написанием теста у нас имеется:

  • понимание, что тест в целом выглядит прежним образом: вызвали -> ожидаем;
  • неизвестное: как мокнуть запрос?
  • неизвестное: как использовать redux инструменты для работы со стором;

Файл с асинхронным тестом:

actions.spec.js

В принципе, на этом единственная трудность unit-тестирования в redux приложении заканчивается. Щепотка нового кода, а именно: использование «хука» afterEach, и пары библиотека для «мокирования» стора и сетевого запроса.

async get news

Код функции getNews

actions.js:


Задание 1: попробуйте сломать тест, изменив ответ в fetch-mock.

Задание 2: допишите тесты на failure случаи.


Полезные материалы по теме


Подписывайтесь на наши сообщества (vkontakte, telegram, youtube, twitter), чтобы быть в курсе годных и актуальных материалов по фронтенду!

p.s. Вебинар и конспект сделан благодаря подписчикам. Было собрано 12 983 рублей.

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

Понравилась статья? Поделиться с друзьями:
Комментариев: 1
  1. Intey

    В плане тестирования редьюсеров я для себя открыл такое:
    [addTodoAct()].reduce(reducer, initialState)

    Разница не большая, но основной профит от того, что мы может протестировать цепочку действий пользователя, просто записав их в нужном порядке. Я так тестирую страницу, где у нас undo/redo.

Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: