В этой статье я хотел бы поделиться своими идеями того, как можно автоматизировать написание unit-тестов в react/redux приложениях. Идеи эти родились в одной из дискуссий с коллегами, в процессе написания тестов, и, как мне кажется, предложенное решение имеет право на жизнь.

О проблеме


Я хотел бы опустить размышления о необходимости unit-тестов и сразу перейти к делу. Как мы сейчас тестируем селекторы?

Redux-селектор — это функция, которая принимает состояние приложения (store) и возвращает разультат. Даже если селектор создан при помощи reselect createSelector() и комбинирует несколько селекторов, на вход он всё также принимает store.

Соответственно, чтобы протестировать селектор, по хорошему, нужно передать в него полный стор. Конечно, создавая mock store можно исключить ненужные области, но если мы тестируем сложный селектор, который комбинирует в себе другие селекторы из разных частей стора, то придется воссоздать полное или почти полное состояние. И так для каждого кейса.

Тут можно пойти несколькими путями:

  • Воссоздать состояние приложения в тестовом окружении и выгрузить состояние, воспользовавшись, к примеру, расширением redux devtools;
  • Просто создать объект, что называется, ручками. Если приложение большое и его состояние содержит много «веток», это может быть достаточно сложным и кропотливым процессом.

Идея автоматизации процесса


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

  • Произошедшее событие (action);
  • Состояние приложения (store);
  • Результат выполнения всех селекторов с этим состоянием;

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

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

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

Как это работает?


Если в целом идея ясна (и кажется вам адекватной :) ), предлагаю приступить к имплементации. Для начала нам потребуется браузерное расширение Redux CheckState.
Это расширение получает все экшны вашего приложения, выполняет селекторы и сохраняет тест-кейсы. В конечном итоге там вы нажимаете на кнопочку и скачиваете файл с получившимися тест-кейсами.

Выглядит это примерно так:

Redux CheckState Screenshot

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

Шаг 1. Экспорт селекторов


В корне проекта нужно создать файл checkState.config.js и из него экспортировать все селекторы, которые вы хотели бы протестировать. В моем тестовом проекте это выглядит так:

export {
    selectCategories,
    selectActiveCategory,
    selectIsCategoryActive,
    selectActiveCategoryId,
} from "./state/category";
export {
    selectPopup,
    selectPopupType,
    selectIsPopupOpen,
} from "./state/popup";
export {
    selectTasks,
    selectActiveTasks,
    selectActiveDoneTasks,
    selectActiveInProgressTasks,
} from "./state/task";

Посмотреть пример на github.

Шаг 2. Имплементация middleware


Теперь нужно добавить middleware, которая будет передавать все экшны и прочие данные в расширение.

Код предельно простой:

import * as selectors from "./checkState.config";

const checkStateMiddleware = (options = {}) => {
    return window && window["__checkStoreExtension__"] ? window["__checkStoreExtension__"](options) :
        store => next => action => next(action);
};

В моем тестовом приложении вы можете посмотреть также вариант имплементации на typescript.

Всё, написание кода на этом завершено. Теперь запускаем приложение, открываем расширение и начинаем пользоваться приложением как пользователь. Нужно совершить как можно больше экшнов. Каждый совершенный экшн вы будете видеть в расширении. Вы так же можете кликнуть на любой экшн и увидеть результаты выполнения селекторов.

Когда все экшны совершенны — просто скачиваете файл и помещаете его в проект. Теперь осталось только запустить тесты. Тут всё еще проще.

Запуск тестов


Для запуска тестов я подготовил CLI tool. Установим пакет:
npm i check-state -g

После чего в папке проекта выполним компанду:
check-state start

Check state CLI найдет сгенерированный браузерным расширением файл с тест-кейсами, найдет и скомпилирует экспортированные селекторы (пока поддерживается javascript и typescript).
После этого последовательно будут пройдены все тест кейсы, каждый селектор будет выполнен с состоянием приложения из тест-кейса и посчитанное значение будет сравниваться с ожидаемым (также из тест-кейса). Если различия нет — мы увидим зеленую строчку, если есть — красную, с информацией, которая поможет диагностировать проблему:

  • Название селектора, который вернул неверный результат;
  • Ожидаемый результат;
  • Текущий результат;
  • Слепок состояния приложения из тест-кейса.

Check state CLI tool screenshot
Пример «упавшего» теста.

Для того, чтобы вы смогли поэкспериментировать с инструментом — я заготовил тестовое приложение, в котором есть несколько селекторов и уже имплементирован Check state: example application

Заключение


Надеюсь, вам понравилась идея автоматизации написания автотестов, и, быть может, вы внедрите этот подход в ваш проект :)

Если вам интересна техническая реализация инструментов:


Буду рад идеям и замечаниям :)

Комментарии (4)


  1. fenomin
    11.02.2019 12:56

    Авторы reselect предлагают тестировать комбайнер функцию, в случае, если зависимости сложные: github.com/reduxjs/reselect#q-how-do-i-test-a-selector

    Ваш подход не плох.

    Но, ИМХО:
    1. Про TDD можно забыть
    2. Сложно будет поддерживать такие тесты
    3. В сложном приложении будут проблемы с тестированием экзотических кейсов


    1. Podpole Автор
      11.02.2019 16:16

      Всё верно, данный подход не совместим с идеей TDD. Про поддержку я уже думаю, вероятно функционал будет расширен и можно будет обновить тесты прямо из CLI, если добавились новые поля в стор, или поменялись результаты селекторов.
      Касательно экзотических кейсов, вы имеете в виду, что сложно в браузере воспроизвести такой кейс?


      1. fenomin
        11.02.2019 16:22

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


        1. Podpole Автор
          11.02.2019 16:40

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

          Вероятно, можно будет подумать и над решением этой проблемы, в дальнейшем…