В этой статье я хотел бы поделиться своими идеями того, как можно автоматизировать написание unit-тестов в react/redux приложениях. Идеи эти родились в одной из дискуссий с коллегами, в процессе написания тестов, и, как мне кажется, предложенное решение имеет право на жизнь.
Я хотел бы опустить размышления о необходимости unit-тестов и сразу перейти к делу. Как мы сейчас тестируем селекторы?
Redux-селектор — это функция, которая принимает состояние приложения (store) и возвращает разультат. Даже если селектор создан при помощи reselect createSelector() и комбинирует несколько селекторов, на вход он всё также принимает store.
Соответственно, чтобы протестировать селектор, по хорошему, нужно передать в него полный стор. Конечно, создавая mock store можно исключить ненужные области, но если мы тестируем сложный селектор, который комбинирует в себе другие селекторы из разных частей стора, то придется воссоздать полное или почти полное состояние. И так для каждого кейса.
Тут можно пойти несколькими путями:
Представьте, что у вас есть middleware, которая, кроме прочего, знает обо всех селекторах приложения. Такая middleware может после каждого экшна вычислить все селекторы и подготовить некий тесткейс, состоящий из:
Таким образом, по каждому экшну мы будем иметь набор данных: состояние приложения для передачи в селекторы и ожидаемые результаты — собственно, чтобы сравнивать.
Т.е. мы имеем наборы данных из реального приложения, на подготовку которых затрачено минимум времени.
Остается только научиться сохранять эти данные и как-то автоматизированно выполнять селекторы и сравнивать результат, но задача эта простая, техническая, и мной для вас решенная. В данной главе я лишь хотел донести идею.
Если в целом идея ясна (и кажется вам адекватной :) ), предлагаю приступить к имплементации. Для начала нам потребуется браузерное расширение Redux CheckState.
Это расширение получает все экшны вашего приложения, выполняет селекторы и сохраняет тест-кейсы. В конечном итоге там вы нажимаете на кнопочку и скачиваете файл с получившимися тест-кейсами.
Выглядит это примерно так:
Для того, чтобы расширение получало данные о происходящих экшнах и могло выполнить силекторы, вам нужно произвести небольшие манипуляции с проектом.
В корне проекта нужно создать файл checkState.config.js и из него экспортировать все селекторы, которые вы хотели бы протестировать. В моем тестовом проекте это выглядит так:
Посмотреть пример на github.
Теперь нужно добавить middleware, которая будет передавать все экшны и прочие данные в расширение.
Код предельно простой:
В моем тестовом приложении вы можете посмотреть также вариант имплементации на typescript.
Всё, написание кода на этом завершено. Теперь запускаем приложение, открываем расширение и начинаем пользоваться приложением как пользователь. Нужно совершить как можно больше экшнов. Каждый совершенный экшн вы будете видеть в расширении. Вы так же можете кликнуть на любой экшн и увидеть результаты выполнения селекторов.
Когда все экшны совершенны — просто скачиваете файл и помещаете его в проект. Теперь осталось только запустить тесты. Тут всё еще проще.
Для запуска тестов я подготовил CLI tool. Установим пакет:
После чего в папке проекта выполним компанду:
Check state CLI найдет сгенерированный браузерным расширением файл с тест-кейсами, найдет и скомпилирует экспортированные селекторы (пока поддерживается javascript и typescript).
После этого последовательно будут пройдены все тест кейсы, каждый селектор будет выполнен с состоянием приложения из тест-кейса и посчитанное значение будет сравниваться с ожидаемым (также из тест-кейса). Если различия нет — мы увидим зеленую строчку, если есть — красную, с информацией, которая поможет диагностировать проблему:
Пример «упавшего» теста.
Для того, чтобы вы смогли поэкспериментировать с инструментом — я заготовил тестовое приложение, в котором есть несколько селекторов и уже имплементирован Check state: example application
Надеюсь, вам понравилась идея автоматизации написания автотестов, и, быть может, вы внедрите этот подход в ваш проект :)
Если вам интересна техническая реализация инструментов:
Буду рад идеям и замечаниям :)
О проблеме
Я хотел бы опустить размышления о необходимости unit-тестов и сразу перейти к делу. Как мы сейчас тестируем селекторы?
Redux-селектор — это функция, которая принимает состояние приложения (store) и возвращает разультат. Даже если селектор создан при помощи reselect createSelector() и комбинирует несколько селекторов, на вход он всё также принимает store.
Соответственно, чтобы протестировать селектор, по хорошему, нужно передать в него полный стор. Конечно, создавая mock store можно исключить ненужные области, но если мы тестируем сложный селектор, который комбинирует в себе другие селекторы из разных частей стора, то придется воссоздать полное или почти полное состояние. И так для каждого кейса.
Тут можно пойти несколькими путями:
- Воссоздать состояние приложения в тестовом окружении и выгрузить состояние, воспользовавшись, к примеру, расширением redux devtools;
- Просто создать объект, что называется, ручками. Если приложение большое и его состояние содержит много «веток», это может быть достаточно сложным и кропотливым процессом.
Идея автоматизации процесса
Представьте, что у вас есть middleware, которая, кроме прочего, знает обо всех селекторах приложения. Такая middleware может после каждого экшна вычислить все селекторы и подготовить некий тесткейс, состоящий из:
- Произошедшее событие (action);
- Состояние приложения (store);
- Результат выполнения всех селекторов с этим состоянием;
Таким образом, по каждому экшну мы будем иметь набор данных: состояние приложения для передачи в селекторы и ожидаемые результаты — собственно, чтобы сравнивать.
Т.е. мы имеем наборы данных из реального приложения, на подготовку которых затрачено минимум времени.
Остается только научиться сохранять эти данные и как-то автоматизированно выполнять селекторы и сравнивать результат, но задача эта простая, техническая, и мной для вас решенная. В данной главе я лишь хотел донести идею.
Как это работает?
Если в целом идея ясна (и кажется вам адекватной :) ), предлагаю приступить к имплементации. Для начала нам потребуется браузерное расширение Redux CheckState.
Это расширение получает все экшны вашего приложения, выполняет селекторы и сохраняет тест-кейсы. В конечном итоге там вы нажимаете на кнопочку и скачиваете файл с получившимися тест-кейсами.
Выглядит это примерно так:
Для того, чтобы расширение получало данные о происходящих экшнах и могло выполнить силекторы, вам нужно произвести небольшие манипуляции с проектом.
Шаг 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: example application
Заключение
Надеюсь, вам понравилась идея автоматизации написания автотестов, и, быть может, вы внедрите этот подход в ваш проект :)
Если вам интересна техническая реализация инструментов:
- Браузерное расширение Redux CheckState на github;
- CLI tool Check state на github.
Буду рад идеям и замечаниям :)
fenomin
Авторы reselect предлагают тестировать комбайнер функцию, в случае, если зависимости сложные: github.com/reduxjs/reselect#q-how-do-i-test-a-selector
Ваш подход не плох.
Но, ИМХО:
1. Про TDD можно забыть
2. Сложно будет поддерживать такие тесты
3. В сложном приложении будут проблемы с тестированием экзотических кейсов
Podpole Автор
Всё верно, данный подход не совместим с идеей TDD. Про поддержку я уже думаю, вероятно функционал будет расширен и можно будет обновить тесты прямо из CLI, если добавились новые поля в стор, или поменялись результаты селекторов.
Касательно экзотических кейсов, вы имеете в виду, что сложно в браузере воспроизвести такой кейс?
fenomin
Да, возможны кейсы, когда на локальном энвайронменте какие-то кейсы могут быть недоступны для воспроизведения. Они могут требовать специфичных ответов от бэкенда и тд.
Podpole Автор
В данном случае — да. Не возможно (или неоправданно сложно) протестировать поведение, которое не воспроизводится локально или на тестовом окружении. В целом и разработка таких систем затрудняется той-же причиной.
Помню, как работая над внутренним приложением одного из крупнейших российских банков — приходилось переносить системный блок в другую часть здания, чтобы попасть в нужную подсеть и получить нужный ответ от сервиса.
Вероятно, можно будет подумать и над решением этой проблемы, в дальнейшем…