Тесты позволяют явно увидеть нам изменения в поведении приложения при изменении его логики (тест выдаст ошибку, если наши изменения в приложении ломают логику другой функции).
Цель тестирования - проверка соответствия ПО предъявляемым требованиям
Виды тестирования:
Функциональное
Модульное (unit) (70%)
Интеграционное (20%)
end-to-end (10%)
Нефункциональное
нагрузочное тестирование
регрессионное (тестирование старого функционала)
тестирование безопасности
Unit-тесты. Они пишутся на отдельные, независимые, маленькие кусочки системы (например, на методы). Они выполняют простую функцию - проверить работу отдельного маленького кусочка.
Screenshot-тесты. Они уже позволяют нам проверять интерфейс приложения.
Если мы поменяем шрифт в одном месте и он поменятся в другом, то такой тест нам сообщит, что изменения произошли в разных местах и выдаст результат.
Integration-тестирование. Оно уже позволяет просмотреть работу нескольких методов в связке: один компонент рендерит внутри себя список других компонентов по запросу
E2E-тестирование. Оно уже предназначено для проверки важных модулей системы:
авторизация
оплата
создание сущностей
удаление записи
Квадрат тестирования - это квадрат, который описывает валидные значения, которые может вернуть функция:
Валидные значения
Пограничные значения
Невалидные значения
И вот пример использования квадрата:
Технологии тестирования:
Jest - самая популярная библиотека для написания любых тестов
React-testing-library - библиотека для тестрования React-приложений
WebdriverIO - E2E-тесты
Storybook + Loki - скриншотные-тесты
Практика. unit тесты с JEST
Устанавливаем в проект библиотеку для тестирования Jest
Далее напишем функцию, которую нужно проверить
validateValue.js
И далее нам нужно написать сами тесты для функции:
test() (мы так же можем писать it() который является алиасом для test()) принимает в себя имя и функцию, которая будет исполнять тестирование
expect() - основная функция, которая используется в тестах - в ней мы описываем, что мы ожидаем от выполнения операции. Внутрь неё передаём операцию и дальше по чейну нужно выбрать одну из функций-проверки
toBe() принимает в себя то значение, которое должно оказаться в expect для успешного прохождения проверки
validateValue.spec.js
Так же мы можем описать сразу несколько тестов для нужного нам функционала:
validateValue.spec.js
Так же проверяем пограничные значения
Так же дальше мы можем попробовать сравнить массивы:
mapArrToString.js
Тут мы уже используем другие функции jest:
toEqual проводит глубокое сравнение объектов и позволяет сравнить массивы и объекты (если попробовать через toBe, то он выдаст феил, так как он будет сравнивать ссылки, а не значения)
чейн not инвертирует результат следующей операции (если у нас значения не эквивалентны, то not выдаст, что они эквивалентны)
mapArrToString.spec.js
Далее реализуем функционал возведения в степень
square.js
Так же условий сравнения в методе expect крайне большое количество и все из них можно посмотреть в документации
Так же jest предоставляет нам 4 функции, которые выполняют побочные действия между тестами:
beforeAll
beforeEach
afterAll
afterEach
square.spec.js
И теперь каждый тест триггерит изменение нашего значения вышеописанными методами
Моковые данные
Модифицируем нашу функцию таким образом, чтобы она возводила числов в степень через метод и могла не вызвать эту функцию, если число = 1
square.js
Для того, чтобы посмотреть сколько раз вызовется определённая функция, мы можем:
воспользоваться jest.spyOn, куда мы передадим библиотеку, за которой следим и её метод
далее нам нужно вызвать целевую функцию
и далее в expect передать наше моковое значение, где чейном проверяем количество вызовов
square.spec.js
Однако тут нужно сказать, что моковые значения в Jest копятся и не делятся на тесты, поэтому второй шан тест уже выдал ошибку
square.spec.js
Чтобы определить нормальное поведение, нужно будет после каждого теста чистить моковые данные с помощью jest.clearAllMocks()
И далее познакомимся с некоторыми особенностями тестирования в реакте:
render() - функция, которая рендерит нужный нам элемент на странице
screen - объект, который хранит в себе вёрстку, которая должна пойти на страницу
getByText - получение элемента по тексту внутри него
getByRole - получение элемента по его роли на странице (инпут, кнопка, артикль)
getByPlaceholderText - получение элемента по тексту плейсхолдера
toBeInTheDocument - проверяет, что элемент должен находиться на странице
App.test.js
При использовании screen.debug() вся вёрстка, которая попадает в screen, будет находиться в консоли
И примерно таким образом выглядят снепшоты элементов
findBy, getBy, queryBy. Пример с useEffect. Асинхронный код
Функции выборки делятся на следующие группы:
getBy... - находит и возвращает элемент, но если не находит, то прокидывает ошибку и тест завершается с фейлом (getAll... возвращает массив элементов)
queryBy... - находит и возвращает элемент, но если элемент не найден, то он нам даёт в этом просто убедиться присвоив в переменную null (queryAll... возвращает массив элементов)
findBy... - работает как queryBy..., но возвращает промис от поиска, то есть работает асинхронно (findAll... возвращает массив элементов)
App.test.js
Уже в данном примере мы проводим поиск через findByText, который позволяет проверить наш асинхронный код появления текста text на странице
Так же, чтобы проверить наличие определённых стилей на элементе, мы можем воспользоваться toHaveStyle()
Далее реализуем переключение состояния отображения элемента (если toggle активен, то будет показываться элемент)
Так же тут будет передан data-testid, который позволит получить доступ к элементу через присвоенный ему id
Далее тут мы будем проверять работу тугглера кнопки:
getByTestId позволяет получать элементы по ранее описанному атрибуту data-testid
fireEvent позволяет триггерить определённые ивенты на выбранных нами элементах
тут мы используем queryByTestId потому что элемент пропадает со страницы и значение будет закономерно null. Вынести один раз в переменную наш элемент, который исчезает нельзя и нам нужно будет его каждый раз получать через вызов screen.queryByTestId('toggle-element')
Тут с помощью fireEvent.input идёт проверка ввода значения в поле ввода. С помощью toContainHTML можно проверить, содержит ли данный HTML-элемент нужное нам значение
Так же у нас имеется модуль userEvent, который выполняет полноценные действия пользователя на странице (двойной клик, наведение мыши, вставка и так далее). Он уже выполняет не искусственные действия, как fireEvent, а реальные действия пользователя
И примерно так будет выглядеть данный тест, но уже с userEvent:
Тестирование компонента с асинхронной загрузкой данных с сервера
Интеграционное тестирование в связке с react router dom v6
Тестируем переход по ссылкам
Начальная сборка для тестирования роутинга:
две страницы
роутинг в App
BrowserRouter в index
И таким способом мы можем проверить наш роутинг:
в компонент MemoryRouter помещаем рендеримый компонент
далее через userEvent.click переходим по страницам
Тестируем попадание на страницу Not Found
Страница ошибки:
Добавляем роут на переход по неизвестной ссылке /*, по которому будет выпадать страница 404
И в тесте мы можем сразу указать страницу, на которой мы должны отрендериться через передачу атрибута initialEntries в MemoryRouter
Конкретно тут написан любой роут, которого нет в приложении, чтобы отрендерился роут ошибки
Тестирование перехода на сгенерированные страницы
Первым делом, создадим страницу отдельного пользователя (данные для всех будут одинаковыми и статичными)
pages > UserDetailsPage.jsx
Список пользователей будет сгенерирован ссылкой
Users.js
Добавляем ссылку на нужного пользователя
App.js
И для того, чтобы протестировать пользователя придётся уже вынести внутрь MemoryRouter оба роута, по которым мы хотим переходить, так как наш компонент App не рендерится и мы из него не можем перейти на нужную страницу user. Так же нужно будет указать /user в путях MemoryRouter, чтобы сразу попасть на нужную нам страницу
Users.tests.js
Хелпер для удобного тестирования роутинга
Первым делом, вынесем всё наше дерево роутинга в отдельный компонент
src > router > AppRouter.jsx
Далее создадим хелпер, который будет в себя принимать ту конструкцию, которая требуется для тестирования нужных роутов приложения
`src > test > helpers > renderWithRouter.jsx
Так выглядит целевой компонент для тестирования:
Users.jsx
Тут оставляем компонент с роутами
App.jsx
Теперь в тестах для запуска тестирования нужного роута, достаточно обернуть нужный компонент в renderWithRouter() хелпер
Users.test.js
Тестирование навбара приложения
Навбар будет хранить просто ссылки по страницам в нашем приложении реакта
Navbar.jsx
В основном компоненте приложения добавляем наш навбар
App.js
И тут пишем отдельные тейки тестирования под срабатывание разных роутов
Navbar.test.js
Интеграционное тестирование в связке с Redux toolkit
Первым делом, нам нужно создать стор
store > store.js
Далее нужно будет создать срез, который вернёт нам редьюсер и два экшена
`store > selectors > counterReducer.js
Тут мы реализуем селектор, по которому мы будем получать значение
если мы ничего не передали в стейт, то значение будет идти от 0 при каждом действии
если мы что-то передали в стейт, то на него будет действовать экшен (increment, decrement)
`store > selectors > counterReducer.test.js
Далее тестируем сам компонент счётчика.
Чтобы достучаться до него и проверить редакс, нужно передать компонент внутри провайдера
В провайдере можно задать начальное значение стейта, если нужно
render() можно использовать просто как функцию и доставать все методы выборки с помощью screen, а можно достать из render его функции выборки и использовать их без обращения к screen (как удобнее, так и делаем, но во втором случае мы не сможем пользоваться выборкой из screen)
функция сравнения toHaveTextContent позволяет нам найти содержание текста в определённом элементе
`components > Counter > Counter.test.jsx“
Хелпер для удобного тестирования компонентов, в которых используется Redux
Мы можем создать такой же хелпер, который и создавали для роутинга, но для редакса
Однако, мы можем в хелперах заранее возвращать сгенерированный ответ от render, а не делать рендер в тесте
test > helpers > renderWithRedux.js
И использовть его для тестирования
Counter.test.js
И так уже будет выглядеть хелпер для тестирования роутинга и редакса одновременно
test > helpers > renderTestApp.js
А так выглядит его применение в приложениях:
Counter.test.js
Если нам нужно будет протестировать асинхронные экшены, то мы просто ровно так же мокаем данные и вписываем ожидания
e2e тесты с WebdriverIO
E2E-тесты, в свою очередь, уже будут запускаться на реальных данных в реальном браузере
Для тестирования приложения будет использоваться WebdriverIO, который представляет из себя движок для тестирования веб-приложений
Так выглядит спек установки вебдрайвера:
Далее добавляем порт в конфиг
И так же у нас появилась папка с готовыми тестами
Тут появится команда запуска тестов
Создадим страницу, которая будет выполнять функционал тугглера текста
pages > HelloWorld.jsx
И добавляем её роут
PageObject паттерн
В данном файле находится главный класс, который будет предоставлять метод открытия компонента в браузере
tests > pages > page.js
Далее пишем текст для страницы приветствия, где мы сначала получаем нужные элементы для взаимодействия, а затем пишем сами тесовые методы, которые должны будут выполниться
Получение элементов происходит через конструкцию $(), в которую мы, используя селекторы $, #, ., получаем нужные нам элементы
tests > pages > hello.e2e.js
В данном файле уже описываем сам тест, используя методы класса
tests > e2e > hello.e2e.js
Запускаем тест
Пример е2е теста с асинхронным кодом
Пишем компонент получения списка пользователей
components > UsersForTest > UsersForTest.js
Далее создаём отдельный компонент пользователя
`components > UsersForTest > User.js
Далее добавляем путь в роутер
router > AppRouter.jsx
Далее тут создаём метод страницы пользователей
tests > pages > users.page.js
А тут пишем сам тест, который будет проверять выполнение методов страницы
`tests > e2e > users.e2e.js
Скриншотные тесты storybook и loki js
Для начала, установим в проект storybook
Далее создастся подобная структура папки с историей
И тут мы можем запустить сторибук
В интерфейсе сторибука мы можем просматривать отдельно каждый из наших компонентов и их спецификации для изменения