Spring Boot
Теория: Интеграционные тесты
Тестирование приложений на Spring Boot — неотъемлемая часть профессиональной жизни веб-разработчиков на Java. Сюда входит написание различных тестов:
- Юнит-тестов для отдельных модулей
- Интеграционных тестов, проверяющих работоспособность всего приложения
В этом уроке мы научимся создавать интеграционные тесты для наших веб-приложений на Spring Boot.
Интеграционное тестирование веб-приложений устроено сложнее, чем тестирование библиотечного кода, где мы вызываем какие-то методы и смотрим на результат.
Веб-приложения работают по сети, обрабатывая HTTP-запросы. Такое поведение придется повторять прямо в тестах или как-то имитировать. Spring Boot позволяет использовать оба подхода. Мы остановимся на подходе с подменой веб-сервера, чтобы ускорить запуск и выполнение тестов. В остальном эти тесты проверяют работу приложения от запроса до ответа, что дает очень высокую степень уверенности в том, что приложение работает.
Для работы с тестами нужно установить зависимости:
Кроме классического Junit, здесь мы видим пакеты, специфичные для Spring Boot. Они дают все необходимые инструменты, чтобы мы могли писать тесты легко и эффективно.
Первый тест
Интеграционные тесты в Spring Boot связаны с маршрутами. Каждый тест — это запрос на конкретный адрес для тестирования конкретного маршрута. Количество тестов для одного маршрута может быть разным, но конкретный тест — это всегда запрос-ответ.
Начнем с примера. Предположим, что у нас есть маршрут /api/users, который возвращает список пользователей. Тест на такой маршрут должен выполнить запрос на этот адрес. Вот как будет выглядеть структура файлов в этом случае:
Тесты Spring Boot расположены в директории src/test/java/io/hexlet/spring. Интеграционные тесты фактически повторяют структуру контроллеров, поэтому удобнее всего делать прямое соответствие между структурой контроллеров и тестами. В примере выше мы видим одни и те же директории. Название теста получается из названия контроллера с добавлением Test в название файла.
Сам тест выглядит так:
Файл тестов — это классический JUnit-класс, в котором тестовые методы помечены аннотациями @Test. Все остальное — это уже специфика Spring Boot. Сюда относятся аннотации @SpringBootTest и @AutoConfigureMockMvc. Во время старта тестов Spring Boot читает эти аннотации, стартует приложение и конфигурирует его в соответствие с аннотациями. Например, нам становится доступным объект mockMvc, через который можно выполнять HTTP-запросы к нашему приложению. Разберем по шагам:
- Метод
get("/api/users")формирует объект запроса к указанной странице. Кроме запросаget, мы можем выполнить любой другой запрос - Метод
mockMvc.perform()выполняет сформированный запрос. На самом деле здесь не происходит HTTP-вызова — запрос передается в приложение напрямую, поэтому тесты работают быстрее, чем с реальным веб-сервером - Метод
andExpect(status().isOk())проверяем, что в ответ вернулся ответ 200. По необходимости можно проверить любой другой статус
Проверка на код ответа считается одной из базовых проверок. Она показывает, что код в целом отработал ожидаемо. При этом мы не можем с уверенностью сказать, что все правильно.
Например, мы ожидаем, что в теле ответа будет JSON определенной структуры, но вдруг там ничего нет? Для контроля ответа нужно добавить проверку тела ответа. Сделать это можно множеством разных способов и библиотек, мы используем следующие:
Использование выглядит так:
Библиотека JsonUnit обладает широкими возможностями по проверке того, как устроен JSON. Подробнее с этими возможностями можно ознакомиться в официальной документации. Изучим несколько примеров:
И последний шаг — запуск тестов:
Взаимодействие с базой
Пример теста списка пользователей не включает в себя одну важную деталь — наполнение базы данных. По умолчанию тесты используют ту базу данных, которая указана в конфигурации. За ее наполнение отвечает программист, а не Spring Boot. Кроме наполнения базы, нам нужна еще и ее очистка.
Представьте, что мы написали тест, который создает пользователя. Если после теста мы не удалим этого пользователя, то следующий тест может завершиться с ошибкой — он не рассчитывает, что в базе уже есть такие данные. По этой причине в большинстве фреймворков каждый тест выполняется в отдельной транзакции, которая откатывается в конце теста. Таким образом достигается полная изоляция тестов друг от друга.
Можно наполнить базу данных, написав пачку SQL-запросов, но это неудобно и сложно в поддержке, особенно на больших объемах. Было бы удобнее, если бы могли автоматически создавать объекты на базе сущностей и сохранять их в базу. В Java есть специальная библиотека — Instancio.
Посмотрим на работу такого теста на примере запроса, обновляющего пользователя. Для этой операции используем маршрут /api/users/{id}. Для выполнения запроса нам понадобится идентификатор пользователя, которого мы создадим с помощью библиотеки Instancio.
Для начала установим необходимые зависимости:
Теперь посмотрим готовый тест, а затем разберем его:
Шаг 1. Сначала мы создаем пользователя. Instancio делает это автоматически, базируясь на полях переданной модели. По умолчанию данные создаются для всех полей, но это не всегда удобно. Во-первых, не нужно заполнять значение для идентификатора, во-вторых, email должен быть настоящим, поэтому здесь мы используем кастомизацию и добавляем адрес с помощью Faker:
Шаг 2. Затем мы подготавливаем запрос. Сначала формируем объект с данными, затем преобразуем их в JSON и устанавливаем соответствующий заголовок. В самом запросе формируем правильный адрес, подставляя идентификатор созданного пользователя:
Шаг 3. Выполняем запрос и проверяем, что он действительно изменил пользователя в базе данных:
Кроме изменения данных в базе, имеет смысл протестировать ответ, который возвращается после запроса.
Обратите внимание на важную деталь, связанную с интеграционными тестами. На протяжении урока мы писали тесты и убеждались, что приложение работает, даже не посмотрев на реализацию самого приложения. В этом и заключается суть интеграционных тестов. Нам не важно, как написано приложение внутри — мы убеждаемся только в том, что оно работает правильно. Из-за этого интеграционные тесты очень устойчивы к изменениям в коде, они меняются в основном из-за изменений API.



