Spring Boot
2026-02-26 19:46 Diff

Тестирование приложений на 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.