[Перевод] Эффективный подход к тестированию веб-контроллеров в Spring Boot приложениях
Команда Spring АйО перевела статью о том, как правильно подходить к тестированию веб-контроллеров в Spring Boot приложениях, чтобы не делать лишнюю работу, но и не упускать важных аспектов процесса тестирования.
Тестирование контроллеров в Spring Boot приложениях похоже на исполнение циркового трюка на удержание равновесия.
Вам хочется быстрых, точных тестов, которые находят реальные проблемы, не пропускают критические ошибки и при этом не замедляют общий прогон тестов. Слишком часто разработчики попадают в одну из двух ловушек: они либо пишут простые юнит-тесты с использованием Mockito и JUnit, либо сразу берутся за полноразмерный @SpringBootTest
.
Ни то, ни другое не дает желаемого результата. В этой статье я покажу вам почему — и как —@WebMvcTest
обеспечивает идеальный баланс, сохраняя как семантику HTTP, так и легкость и мощность ваших тестов.
Ловушки простых юнит-тестов
Давайте начнем с самого распространенного решения: простой юнит-тест для вашего контроллера с использованием JUnit и Mockito. Вы чувствуете искушение воспользоваться таким способом — сделать mock сервисного слоя, вызвать метод контроллера напрямую и применить assert
к результату. Быстро, изолированно, готово. Верно?
Не совсем. Когда вы тестируете контроллер таким способом, вы теряете нечто критически важное: семантику HTTP запроса. Контроллеры в Spring Boot — это не просто какие-то классы, они являются точками входа в ваше веб приложение, обрабатывающими запросы, ответы, коды статуса, заголовки и безопасность.
Обход HTTP-слоя в обычных юнит-тестах означает, что вы не тестируете, как ваш контроллер функционирует в реальных условиях.
Вот что вы пропускаете:
Маппинг запросов: вы уверены, что
/api/users/{id}
действительно указывает на ваш метод?Коды статуса: какой код возвращает bad request, 400 или случайный 200?
Заголовки: правильно ли вы посылаете
Content-Type
или кастомизированные заголовки?Безопасность: всегда ли выполняется ваше правило
@PreAuthorize
?
Использование Mockito для всего подряд может помочь проверить логику вашего метода, но теряет то самое связующее вещество, которое делает контроллер контроллером. Вы тестируете пустую болванку, а не реальный продукт.
Почему @SpringBootTest — это перебор
С другой стороны, некоторые разработчики идут слишком далеко в противоположном направлении и используют @SpringBootTest
. Эта аннотация приводит в действие все ваше Spring Boot приложение полностью — базу данных, сервисы, бины, вообще все.
Конечно, она протестирует ваш контроллер в «реальном» окружении, вместе с семантикой HTTP при помощи TestRestTemplate
или WebTestClient
. Но это все равно, что использовать кувалду, чтобы расколоть грецкий орех.
А проблема вот в чем:
Медленный запуск: загрузка всего контекста приложения может занять 20–45 секунд, замедляя работу вашего набора тестов.
Чрезмерный охват: вы тестируете контроллер, а не базу данных и не имеющие отношения к контроллеру бины.
Сложность: отладка ошибок превращается в поиск иголки в стогу сена по всем слоям приложения.
Аннотация @SpringBootTest
прекрасна для интеграционных тестов, для которых вам нужно все приложение, но для тестирования контроллеров? Это перебор. Вам нужна точечность, скорость и достаточная реалистичность — без лишнего багажа.
Золотая середина: @WebMvcTest
В чат входит @WebMvcTest
— то самое решение, которое вам нужно для тестирования Spring Boot контроллеров.
Эта аннотация загружает только веб слой (контроллеры, фильтры и MVC инфраструктуру) заменяя остальную часть приложения на mock-и. Она работает быстро, дает необходимый фокус и, что важнее всего, сохраняет HTTP семантику с помощью MockMvc от Spring.
Вот почему это лучший выбор:
1. Mock-и для HTTP семантики
Работая с MockMvc, вы имитируете реальные HTTP запросы — GET, POST, PUT, DELETE, какие угодно.
Вы не вызываете методы напрямую, а только обращаетесь к эндпоинтам, как как это делает клиентское приложение.
Это позволяет вам проверить:
Маппинг URL-ов: является ли
/api/users
правильным путем?Тело запроса: правильно ли десериализуются входные данные в формате JSON?
Тело ответа: такие ли выходные данные вы ожидали?
2. Статус коды и заголовки
MockMvc дает вам полный контроль, позволяющий проверить параметры, свойственные HTTP:
Статус:
expect(status().isOk())
илиexpect(status().isForbidden())
.Заголовки:
expect(header().string("X-Custom-Header", "value"))
.Тип содержимого:
expect(content().contentType(MediaType.APPLICATION_JSON))
.
Больше не нужно гадать, устанавливает ли ваш контроллер 201 Created и не забывает ли он про заголовок — @WebMvcTest
позаботится обо всем.
3. Security Testing
У вас подключена Spring Security?
@WebMvcTest
готова к таким вызовам. Добавьте .with(user("testuser").roles("USER"))
к вашему запросу и тестируйте:
Аутентификация: отклонит ли эндпоинт запрос без аутентификации?
Авторизация: правильно ли работает ролевая модель @PreAuthorize("hasRole('ADMIN')")
?
Все это можно сделать простым юнит-тестом —, а при использовании @SpringBootTest
такие тесты ужасно медленные.
4. Скорость и фокус
Если загружать только слой MVC и зависимости для mock-ов (например, сервисы с @MockitoBean
), тесты, использующие @WebMvcTest
, остаются достаточно легкими. Здесь нет ни подключений к базе данных, ни посторонних бинов — только сам контроллер и его поведение с точки зрения HTTP.
Пример
Давайте посмотрим, как работает @WebMvcTest
. Представим себе простой контроллер:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
}
Вот как его можно протестировать с использованием @WebMvcTest
:
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private UserService userService;
@Test
void getUser_ReturnsUser_WhenFound() throws Exception {
// Arrange
User user = new User(1L, "Alice");
when(userService.findById(1L)).thenReturn(user);
// Act & Assert
mockMvc.perform(get("/api/users/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.name").value("Alice"));
}
}
Что здесь происходит?
@WebMvcTest
загружает толькоUserController
и MVC компоненты.@MockitoBean
имитируетUserService
, оставляя тест изолированным.MockMvc
посылает GET запрос к/api/users/1
и проверяет статус, тип содержимого и ответ в формате JSON.
Быстро, сфокусированно и с учетом HTTP семантики. Никакого полного запуска всего приложения, никакого игнорирования веб-слоя.
Советы по успешному использованию @WebMvcTest
Mock зависимости: используйте
@MockBean
для сервисов или репозиториев, необходимых вашему контроллеру.Настройки безопасности: добавьте
@WithMockUser
или кастомизированныйSecurityMockMvcRequestPostProcessors
для тестов аутентификации.Обработка ошибок: тестируйте ответы 400, 404 или 500, вводя неверные данные или имитируя ошибки.
Пусть ваши тесты будут легкими: чтобы не загружать ненужные контроллеры, задайте целевой класс (например, @WebMvcTest(UserController.class)
).
Заключение: тестируйте с умом, а не через усилия
Тестирование контроллеров Spring Boot приложения не обязательно должно быть компромиссом.
Простые юнит-тесты с использованием Mockito не воспроизводят HTTP семантику, оставляя прорехи в покрытии, @SpringBootTest
заставляет использовать огромное количество ресурсов, которые вам не нужны. @WebMvcTest
становится золотой серединой — имитирует HTTP, сохраняет настоящие эндпоинты и предоставляет только необходимый контекст, чтобы протестировать важные моменты: коды статуса, заголовки, безопасность и ответы.
В следующий раз, когда вы будете писать тесты контроллеров, не впадайте в крайности и сразу берите @WebMvcTest
. Ваша команда будет вам благодарна. Приятного тестирования.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.