Эффективный подход к тестированию веб-контроллеров в 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-и.

Комментарий от редакции Spring АйО

Важный момент: mock-объекты для других частей приложения не создаются автоматически. Бины из остальной части приложения просто не будут загружены и если контроллер зависит от этих бинов, тест упадёт с ошибкой, потому что Spring не смог создать контроллер. Чтобы всё заработало, разработчик должен сам замокать все зависимости контроллера, как показано в примерах ниже.

Она работает быстро, дает необходимый фокус и, что важнее всего, сохраняет 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<User> 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 и всего, что с ним связано.


ссылка на оригинал статьи https://habr.com/ru/articles/892194/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *