Привет! Здесь собрана информация и примеры для удобной подготовки к тестированию React приложений.
- Тестирование React приложений
- Подготовка
- Вступление
- Частые вопросы
- Почему не стоит тестировать реализацию?
- Как найти нужный DOM элемент?
- Как тестировать состояние или редьюсер компонента?
- Как тестировать
fetchзапросы? - Как тестировать асинхронные функции?
- Почему
getByRoleне находит нужный элемент? - Что за ошибка
Should be wrapped into act? - Что за ошибка
Nock disallowed net connect?
- Полезные материалы
Добавляем необходимые npm пакеты:
- jest
- nock
- @testing-library/jest-dom
- @testing-library/react
- @testing-library/user-event
- babel-jest
- eslint-plugin-jest
- eslint-plugin-jest-dom
- eslint-plugin-testing-library
После этого создаем и настраиваем файлы конфигурации jest.config.js, jest.setup.js, .eslintrc.js, .babelrc. Они уже есть в этом репозитории и их можно использовать в проектах в текущем или доработанном виде
Вызов npm run test запускает одноразовую проверку тестов.
Вызов npm run test:watch запускает слежение за изменениями в файлах тестов и их автоматическое выполнение.
В проекте файлы с тестами располагаются либо в папке своего компонента, либо в папке /tests в корневой директории. Папка /tests содержит тесты страниц (/pages), функций-хелперов (/helpers) и другие.
Название тестового файла компонента:
ComponentName.test.js
Название тестового файла функции:
functionName.test.js
Каждый тест оборачивается в функцию test() с его описанием:
test('should send request on form submit', () => {...})Для объединения тестов, относящихся к одному тестируемому объекту используется функция-обертка describe(). Тестируемым объектом может быть компонент, функция и другие вещи.
describe('functionName', () => {
test('should do something', () => {...})
})Чтобы пропустить один или несколько тестов используется метод skip:
test.skip('...', () => {...})
// ИЛИ
describe.skip('...', () => {...})Название каждого теста должно отражать его цель и действие. В начале ставится слово should, затем идет описание ожидаемого результата и в конце идет действие, которое должно привести к нему. Примеры:
test('should open a modal when click on the button')test('should change city when dispatch CHANGE_CITY action')test('should render comp-t with props')
Тест-кейсы (описываемые функцией describe()) должны называться имененем тестируемого объекта, к которому относятся все тесты внутри. Примеры:
- Компонент:
describe('ComponentName comp-t') - Функция:
describe('functionName function') - Редьюсер компонента:
describe('ComponentName reducer') - Redux-редьюсер:
describe('Redux reducer reducerName')
В первую очередь, работоспособность компонентов и взаимодействие с приложением от лица юзера. Верно ли отредерены props, нажатие на кнопки, заполнение форм, открытие модалок, подзагрузка элементов и др. Тестируем результат нашего кода.
Как компоненты или функции написаны и сделаны. Названия переменных, структура функций, типы props, стили, сторонние библиотеки и др. Не тестируем то, как код реализован.
Сторонние библиотеки имеют свои тесты. В наших мы можем либо полностью заменить их компоненты (например, используя заглушку типа </ div>), либо тестировать только самую необходимую логику (например, отправку форм из Formik).
Для рендера и получения элементов используется библиотека @testing-library/react.
Для проверки элементов на содержимое и состояние используется jest-dom
Для вызова клиентских событий используется @testing-library/user-event.
Для перехвата fetch запросов используется nock.
Основной фреймворк для тестов с базовыми инструментами – jest
В большинстве случаев такой подход ведет к высокому шансу на ошибку и неверный результат. Допустим, мы что-то незначительно поменяли в компоненте. В итоге он всё ещё работает, но устаревший тест теперь будет говорить обратное (False negatives). Или наоборот, в результате изменений компонент может перестать работать, но на тесте это не отразится (False positives). И шансы на такие ошибки возрастают при малейшем рефакторе кода.
Поэтому мы тестируем результат кода, а не его реализацию. Работаем именно с тем, что в конечно итоге дойдет до пользователя и будет непосредственно влиять на его опыт. По этой же причине мы не тестируем снимками. Особенно приятно то, что такой подход не только надежнее, но и проще в реализации и поддержке.
Подробнее на эту тему можно почитать в этом материале.
Используем запросы (queries) от библиотеки @testing-library/react.
-
Для элементов с определенной
ariaролью используем...ByRoleзапросы.
Например:screen.getByRole('button', { name: 'Some text' });
В этом примере мы, во-первых, будем уверены в том, что найденный элемент является именно кнопкой, а во-вторых, проверяем ее же доступность. Список ARIA Roles.
-
Для разных элементов есть более подходящие
queries, чем другие. Например, для полей форм лучше использоватьgetByLabelText. -
Различия между
getBy...,queryBy...иfindBy...:getBy...находит элемент или выбрасывает ошибку, если он не найден.queryBy...находит элемент или возвращаетnull, если он не найден (удобно для проверки на отсутствие).findBy...возвращает выполненный промис c элементов, если он был найден за определенное время (удобно в случае, когда элемент рендерится не сразу).
-
Для получения сразу нескольких элементов в виде массива используются
getByAll...,queryByAll...иfindByAll....
Оригинальные state или reducer не нужно использовать или повторять, проверяем и оцениваем только результат их работы. Например то, что при клике на кнопку должна появиться модалка.
Примеры есть в
/examples/component-with-state
/examples/component-with-reducer
С помощью HTTP перехватчика nock. Эта библиотека помогает перехватить настоящие запросы и вернуть любой желаемый ответ. Она удобна тем, что нам не нужно задумываться, как мокать функцию для вызова запроса, ведь можно работать с ним напрямую.
Например:
nock(BASE_API_URL).get(API_PATH).reply(200, { some_data: [] });Примеры использования nock в тестах:
/examples/component-with-form
/examples/component-with-uploading
В тестах они являются частой причиной поломки, ведь тестирование происходит в синхронном режиме. Правильный подход в том, чтобы дождаться выполнения всех асинхронных колбеков перед завершением теста. Сделать это можно при помощи фунции-промиса await waitFor().
Например, вызываем асинхронную функцию, которая меняет какое-то состояние, и затем проверяем результат ее выполнения в обертке waitFor():
userEvent.click(openDialogButton); // Клик по кнопке с асинхронным обработчиком
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeVisible();
});Такой способ (когда мы ожидаем выполнения каких-то действий в waitFor() после асинхронного колбека) является самым предпочтительным решением. Однако есть и другие варианты:
- Дождаться появления каких-то элементов с помощью асинхронного
findBy.... - Обернуть асинхронное действие в
act(). Но такой способ используется в очень редких случаях. - Поставить вызов
await waitFor(() => {})с пустым колбеком в конец теста. Это заставит тест дождаться завершения всех асинхронных действий перед закрытием. Хоть это и своего рода хак, но иногда его можно использовать.
Функция getByRole используется для поиска элемента с явной ARIA ролью и описанием. Но если у элемента нет описания или он не доступен для пользователя, то эта функция вернёт ошибку. Способы решения:
- Если элемент скрыт – передаем в
getByRoleпараметрhidden: true. - Если у элемента нет описания – добавляем его любым удобным способом. Например, у тега
<form />по умолчанию нет ARIA ролиform, пока у него не будет явного описания. Мы можем это исправить добавив его, например, с помощьюaria-label.
Такая ошибка часто возникает из-за оставшихся асинхронных действий после завершения теста. Её можно встретить при тестах форм под управлением Formik или при вызове асинхронных колбеков. Решение описано выше
Произошел HTTP запрос, не обработанный nock. Все реальные запросы в тестах запрещены, поэтому если появляется данная ошибка, нужно добавить перехватчик запроса перед его вызовом.
Внимание! URL запроса в nock должен совпадать с настоящим, чтобы перехватчик мог его обработать.
В этих материалах могут встречаться другие библиотеки или методы тестирования. Поэтому этот список скорее нужен для ознакомления с правильными подходами к написанию тестов, а за примерами иногда лучше заглянуть в /examples.
- Введение в тестирование (есть устаревшее)
- Современные методы тестирования
- Распространенные ошибки и их решения
- Почему нужно тестировать результат, а не реализацию
- Список
expectметодов библиотеки Jest - Список сопоставителей (
matchers) библиотеки jest-dom
Так же советую почитать статьи от одного из лучших мастеров тестирования – Kent C. Dodds.
В курсе Школы разработки интерфейсов (ШРИ) Яндекса за 2019-2020 год есть две полезные лекции про тестирование, которые помогут получить начальное представление о том, что такое юнит-тесты и интеграционные тесты, о подходах к тестированию и практических приёмах его выполнения.