1 added
1 removed
Original
2026-01-01
Modified
2026-02-26
1
<p>Тестирование приложений на Spring Boot - неотъемлемая часть профессиональной жизни веб-разработчиков на Java. Сюда входит написание различных тестов:</p>
1
<p>Тестирование приложений на Spring Boot - неотъемлемая часть профессиональной жизни веб-разработчиков на Java. Сюда входит написание различных тестов:</p>
2
<ul><li>Юнит-тестов для отдельных модулей</li>
2
<ul><li>Юнит-тестов для отдельных модулей</li>
3
<li>Интеграционных тестов, проверяющих работоспособность всего приложения</li>
3
<li>Интеграционных тестов, проверяющих работоспособность всего приложения</li>
4
</ul><p>В этом уроке мы научимся создавать интеграционные тесты для наших веб-приложений на Spring Boot.</p>
4
</ul><p>В этом уроке мы научимся создавать интеграционные тесты для наших веб-приложений на Spring Boot.</p>
5
<p>Интеграционное тестирование веб-приложений устроено сложнее, чем тестирование библиотечного кода, где мы вызываем какие-то методы и смотрим на результат.</p>
5
<p>Интеграционное тестирование веб-приложений устроено сложнее, чем тестирование библиотечного кода, где мы вызываем какие-то методы и смотрим на результат.</p>
6
<p>Веб-приложения работают по сети, обрабатывая HTTP-запросы. Такое поведение придется повторять прямо в тестах или как-то имитировать. Spring Boot позволяет использовать оба подхода. Мы остановимся на подходе с подменой веб-сервера, чтобы ускорить запуск и выполнение тестов. В остальном эти тесты проверяют работу приложения от запроса до ответа, что дает очень высокую степень уверенности в том, что приложение работает.</p>
6
<p>Веб-приложения работают по сети, обрабатывая HTTP-запросы. Такое поведение придется повторять прямо в тестах или как-то имитировать. Spring Boot позволяет использовать оба подхода. Мы остановимся на подходе с подменой веб-сервера, чтобы ускорить запуск и выполнение тестов. В остальном эти тесты проверяют работу приложения от запроса до ответа, что дает очень высокую степень уверенности в том, что приложение работает.</p>
7
<p>Для работы с тестами нужно установить зависимости:</p>
7
<p>Для работы с тестами нужно установить зависимости:</p>
8
<p>Кроме классического Junit, здесь мы видим пакеты, специфичные для Spring Boot. Они дают все необходимые инструменты, чтобы мы могли писать тесты легко и эффективно.</p>
8
<p>Кроме классического Junit, здесь мы видим пакеты, специфичные для Spring Boot. Они дают все необходимые инструменты, чтобы мы могли писать тесты легко и эффективно.</p>
9
<h2>Первый тест</h2>
9
<h2>Первый тест</h2>
10
<p>Интеграционные тесты в Spring Boot связаны с маршрутами. Каждый тест - это запрос на конкретный адрес для тестирования конкретного маршрута. Количество тестов для одного маршрута может быть разным, но конкретный тест - это всегда запрос-ответ.</p>
10
<p>Интеграционные тесты в Spring Boot связаны с маршрутами. Каждый тест - это запрос на конкретный адрес для тестирования конкретного маршрута. Количество тестов для одного маршрута может быть разным, но конкретный тест - это всегда запрос-ответ.</p>
11
<p>Начнем с примера. Предположим, что у нас есть маршрут<em>/api/users</em>, который возвращает список пользователей. Тест на такой маршрут должен выполнить запрос на этот адрес. Вот как будет выглядеть структура файлов в этом случае:</p>
11
<p>Начнем с примера. Предположим, что у нас есть маршрут<em>/api/users</em>, который возвращает список пользователей. Тест на такой маршрут должен выполнить запрос на этот адрес. Вот как будет выглядеть структура файлов в этом случае:</p>
12
<p>Тесты Spring Boot расположены в директории<em>src/test/java/io/hexlet/spring</em>. Интеграционные тесты фактически повторяют структуру контроллеров, поэтому удобнее всего делать прямое соответствие между структурой контроллеров и тестами. В примере выше мы видим одни и те же директории. Название теста получается из названия контроллера с добавлением<em>Test</em>в название файла.</p>
12
<p>Тесты Spring Boot расположены в директории<em>src/test/java/io/hexlet/spring</em>. Интеграционные тесты фактически повторяют структуру контроллеров, поэтому удобнее всего делать прямое соответствие между структурой контроллеров и тестами. В примере выше мы видим одни и те же директории. Название теста получается из названия контроллера с добавлением<em>Test</em>в название файла.</p>
13
<p>Сам тест выглядит так:</p>
13
<p>Сам тест выглядит так:</p>
14
<p>Файл тестов - это классический JUnit-класс, в котором тестовые методы помечены аннотациями @Test. Все остальное - это уже специфика Spring Boot. Сюда относятся аннотации @SpringBootTest и @AutoConfigureMockMvc. Во время старта тестов Spring Boot читает эти аннотации, стартует приложение и конфигурирует его в соответствие с аннотациями. Например, нам становится доступным объект mockMvc, через который можно выполнять HTTP-запросы к нашему приложению. Разберем по шагам:</p>
14
<p>Файл тестов - это классический JUnit-класс, в котором тестовые методы помечены аннотациями @Test. Все остальное - это уже специфика Spring Boot. Сюда относятся аннотации @SpringBootTest и @AutoConfigureMockMvc. Во время старта тестов Spring Boot читает эти аннотации, стартует приложение и конфигурирует его в соответствие с аннотациями. Например, нам становится доступным объект mockMvc, через который можно выполнять HTTP-запросы к нашему приложению. Разберем по шагам:</p>
15
-
<ul><li>Метод get("/api/users") формирует объект запроса к указанной странице. Кро��е запроса get, мы можем выполнить любой другой запрос</li>
15
+
<ul><li>Метод get("/api/users") формирует объект запроса к указанной странице. Кроме запроса get, мы можем выполнить любой другой запрос</li>
16
<li>Метод mockMvc.perform() выполняет сформированный запрос. На самом деле здесь не происходит HTTP-вызова - запрос передается в приложение напрямую, поэтому тесты работают быстрее, чем с реальным веб-сервером</li>
16
<li>Метод mockMvc.perform() выполняет сформированный запрос. На самом деле здесь не происходит HTTP-вызова - запрос передается в приложение напрямую, поэтому тесты работают быстрее, чем с реальным веб-сервером</li>
17
<li>Метод andExpect(status().isOk()) проверяем, что в ответ вернулся ответ<em>200</em>. По необходимости можно проверить любой другой статус</li>
17
<li>Метод andExpect(status().isOk()) проверяем, что в ответ вернулся ответ<em>200</em>. По необходимости можно проверить любой другой статус</li>
18
</ul><p>Проверка на код ответа считается одной из базовых проверок. Она показывает, что код в целом отработал ожидаемо. При этом мы не можем с уверенностью сказать, что все правильно.</p>
18
</ul><p>Проверка на код ответа считается одной из базовых проверок. Она показывает, что код в целом отработал ожидаемо. При этом мы не можем с уверенностью сказать, что все правильно.</p>
19
<p>Например, мы ожидаем, что в теле ответа будет JSON определенной структуры, но вдруг там ничего нет? Для контроля ответа нужно добавить проверку тела ответа. Сделать это можно множеством разных способов и библиотек, мы используем следующие:</p>
19
<p>Например, мы ожидаем, что в теле ответа будет JSON определенной структуры, но вдруг там ничего нет? Для контроля ответа нужно добавить проверку тела ответа. Сделать это можно множеством разных способов и библиотек, мы используем следующие:</p>
20
<p>Использование выглядит так:</p>
20
<p>Использование выглядит так:</p>
21
<p>Библиотека JsonUnit обладает широкими возможностями по проверке того, как устроен JSON. Подробнее с этими возможностями можно ознакомиться в<a>официальной документации</a>. Изучим несколько примеров:</p>
21
<p>Библиотека JsonUnit обладает широкими возможностями по проверке того, как устроен JSON. Подробнее с этими возможностями можно ознакомиться в<a>официальной документации</a>. Изучим несколько примеров:</p>
22
<p>И последний шаг - запуск тестов:</p>
22
<p>И последний шаг - запуск тестов:</p>
23
<h2>Взаимодействие с базой</h2>
23
<h2>Взаимодействие с базой</h2>
24
<p>Пример теста списка пользователей не включает в себя одну важную деталь - наполнение базы данных. По умолчанию тесты используют ту базу данных, которая указана в конфигурации. За ее наполнение отвечает программист, а не Spring Boot. Кроме наполнения базы, нам нужна еще и ее очистка.</p>
24
<p>Пример теста списка пользователей не включает в себя одну важную деталь - наполнение базы данных. По умолчанию тесты используют ту базу данных, которая указана в конфигурации. За ее наполнение отвечает программист, а не Spring Boot. Кроме наполнения базы, нам нужна еще и ее очистка.</p>
25
<p>Представьте, что мы написали тест, который создает пользователя. Если после теста мы не удалим этого пользователя, то следующий тест может завершиться с ошибкой - он не рассчитывает, что в базе уже есть такие данные. По этой причине в большинстве фреймворков каждый тест выполняется в отдельной транзакции, которая откатывается в конце теста. Таким образом достигается полная изоляция тестов друг от друга.</p>
25
<p>Представьте, что мы написали тест, который создает пользователя. Если после теста мы не удалим этого пользователя, то следующий тест может завершиться с ошибкой - он не рассчитывает, что в базе уже есть такие данные. По этой причине в большинстве фреймворков каждый тест выполняется в отдельной транзакции, которая откатывается в конце теста. Таким образом достигается полная изоляция тестов друг от друга.</p>
26
<p>Можно наполнить базу данных, написав пачку SQL-запросов, но это неудобно и сложно в поддержке, особенно на больших объемах. Было бы удобнее, если бы могли автоматически создавать объекты на базе сущностей и сохранять их в базу. В Java есть специальная библиотека -<a>Instancio</a>.</p>
26
<p>Можно наполнить базу данных, написав пачку SQL-запросов, но это неудобно и сложно в поддержке, особенно на больших объемах. Было бы удобнее, если бы могли автоматически создавать объекты на базе сущностей и сохранять их в базу. В Java есть специальная библиотека -<a>Instancio</a>.</p>
27
<p>Посмотрим на работу такого теста на примере запроса, обновляющего пользователя. Для этой операции используем маршрут<em>/api/users/{id}</em>. Для выполнения запроса нам понадобится идентификатор пользователя, которого мы создадим с помощью библиотеки<em>Instancio</em>.</p>
27
<p>Посмотрим на работу такого теста на примере запроса, обновляющего пользователя. Для этой операции используем маршрут<em>/api/users/{id}</em>. Для выполнения запроса нам понадобится идентификатор пользователя, которого мы создадим с помощью библиотеки<em>Instancio</em>.</p>
28
<p>Для начала установим необходимые зависимости:</p>
28
<p>Для начала установим необходимые зависимости:</p>
29
<p>Теперь посмотрим готовый тест, а затем разберем его:</p>
29
<p>Теперь посмотрим готовый тест, а затем разберем его:</p>
30
<p><strong>Шаг 1</strong>. Сначала мы создаем пользователя. Instancio делает это автоматически, базируясь на полях переданной модели. По умолчанию данные создаются для всех полей, но это не всегда удобно. Во-первых, не нужно заполнять значение для идентификатора, во-вторых, email должен быть настоящим, поэтому здесь мы используем кастомизацию и добавляем адрес с помощью Faker:</p>
30
<p><strong>Шаг 1</strong>. Сначала мы создаем пользователя. Instancio делает это автоматически, базируясь на полях переданной модели. По умолчанию данные создаются для всех полей, но это не всегда удобно. Во-первых, не нужно заполнять значение для идентификатора, во-вторых, email должен быть настоящим, поэтому здесь мы используем кастомизацию и добавляем адрес с помощью Faker:</p>
31
<p><strong>Шаг 2</strong>. Затем мы подготавливаем запрос. Сначала формируем объект с данными, затем преобразуем их в JSON и устанавливаем соответствующий заголовок. В самом запросе формируем правильный адрес, подставляя идентификатор созданного пользователя:</p>
31
<p><strong>Шаг 2</strong>. Затем мы подготавливаем запрос. Сначала формируем объект с данными, затем преобразуем их в JSON и устанавливаем соответствующий заголовок. В самом запросе формируем правильный адрес, подставляя идентификатор созданного пользователя:</p>
32
<p><strong>Шаг 3</strong>. Выполняем запрос и проверяем, что он действительно изменил пользователя в базе данных:</p>
32
<p><strong>Шаг 3</strong>. Выполняем запрос и проверяем, что он действительно изменил пользователя в базе данных:</p>
33
<p>Кроме изменения данных в базе, имеет смысл протестировать ответ, который возвращается после запроса.</p>
33
<p>Кроме изменения данных в базе, имеет смысл протестировать ответ, который возвращается после запроса.</p>
34
<p>Обратите внимание на важную деталь, связанную с интеграционными тестами. На протяжении урока мы писали тесты и убеждались, что приложение работает, даже не посмотрев на реализацию самого приложения. В этом и заключается суть интеграционных тестов. Нам не важно, как написано приложение внутри - мы убеждаемся только в том, что оно работает правильно. Из-за этого интеграционные тесты очень устойчивы к изменениям в коде, они меняются в основном из-за изменений API.</p>
34
<p>Обратите внимание на важную деталь, связанную с интеграционными тестами. На протяжении урока мы писали тесты и убеждались, что приложение работает, даже не посмотрев на реализацию самого приложения. В этом и заключается суть интеграционных тестов. Нам не важно, как написано приложение внутри - мы убеждаемся только в том, что оно работает правильно. Из-за этого интеграционные тесты очень устойчивы к изменениям в коде, они меняются в основном из-за изменений API.</p>