[Перевод] Эффективный подход к тестированию веб-контроллеров в 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. Ваша команда будет вам благодарна. Приятного тестирования.

25848492d4cd2dcd1df595e475569a9a.png

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

© Habrahabr.ru