Как стать автором
Обновить

Пара советов по покрытию тестами проекта на SpringBoot

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров589

Основные принципы

Юнит-тесты — основа тестирования. Тестируйте изолированно отдельные компоненты.

Slice-тесты (@DataJpaTest, @WebMvcTest и т. д.) — используйте для тестирования слоёв приложения с частичным поднятием контекста.

Интеграционные тесты (@SpringBootTest) — только для критических сценариев, где требуется полный контекст Spring.

Целевое соотношение тестов

70% — юнит-тесты.

25% — slice-тесты.

5% — интеграционные тесты.

Интеграционные тесты — это "тяжёлая артиллерия". Чем меньше их в проекте, тем быстрее и стабильнее наши сборки.

Когда что использовать?

Юнит-тесты:

  • Сервисы

  • Утилиты

  • Валидаторы

  • Алгоритмы

Slice-тесты:

Интеграционные тесты:

  • Ну что тут сказать... Лучше их избегать 🙃


Unit-тесты

Общий подход

  • Изолируйте тестируемый класс от зависимостей с помощью Mockito.

  • Используйте @Mock для создания моков зависимостей и @InjectMocks для внедрения их в тестируемый объект.

Как надо

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

  @Mock
  private UserRepository userRepository;

  @InjectMocks
  private UserService userService;

  @Test
  void getUserById_ShouldReturnUser() {
    User expectedUser = new User(1L, "John");
    when(userRepository.findById(1L)).thenReturn(Optional.of(expectedUser));

    User actualUser = userService.getUserById(1L);

    assertEquals(expectedUser, actualUser);
    verify(userRepository).findById(1L);
  }
}

Как не надо

@SpringBootTest // Избыточное поднятие контекста для юнит-теста
class UserServiceTest {

  @Autowired // Зависимость из реального контекста
  private UserRepository userRepository;

  @Autowired
  private UserService userService;

  // Тест становится медленным и зависимым от внешних компонентов. Вместо мокирования придется создавать сущность либо использовать инициализирующий SQL.
}

Slice-тесты

Общий подход

Используйте аннотации, которые поднимают только нужный слой (например, @DataJpaTest для JPA, @WebMvcTest для контроллеров).

Это позволяет проверить как работает функционал, который реализован средствами фреймворка (доступ к данным, HTTP-взаимодействие, кэширование и пр.), не поднимая при этом контекст целиком.

Как надо

@DataJpaTest
class UserRepositoryTest {

  @Autowired
  private TestEntityManager entityManager;

  @Autowired
  private UserRepository userRepository;

  @Test
  void findByEmail_ShouldReturnUser() {
    User user = new User("john@example.com");
    entityManager.persist(user);
    entityManager.flush();

    User found = userRepository.findByEmail("john@example.com");
    assertEquals(user.getEmail(), found.getEmail());
  }
}

Как не надо

@SpringBootTest // Избыточный полный контекст вместо среза
class UserRepositoryTest {

  @Autowired
  private UserRepository userRepository;

  // Тест будет медленным и загрузит ненужные бины
}

Интеграционные тесты

Общий подход

Интеграционные тесты допускается использовать для:

  • Тестирования взаимодействия нескольких модулей.

  • Проверки корректности конфигурации Spring (например, security, миграции БД).

  • Сценариев, которые невозможно покрыть юнитами и slice-тестами

Пример допустимого использования

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserIntegrationTest {

  @Autowired
  private TestRestTemplate restTemplate;

  @Test
  void getUser_ShouldReturn200() {
    ResponseEntity<User> response = restTemplate.getForEntity("/users/1", User.class);
    assertEquals(HttpStatus.OK, response.getStatusCode());
  }
}

Важно помнить, что случаи, когда интеграционный тест действительно необходим - крайне редки.

Альтернатива с использованием Slice

@WebMvcTest(UserController.class)
class UserControllerTest {

  @MockitoBean
  private UserService userService;

  @Autowired
  private MockMvc mockMvc;

  @Test
  void getUser_ShouldReturn200() throws Exception {
    when(userService.getUserById(1L)).thenReturn(new User(1L, "John"));

    mockMvc.perform(get("/users/1"))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.name").value("John"));
  }
}

Повышение читабельности тестов

@DisplayName

Для повышения читабельности тестов целесообразно использовать JUnit аннотацию @DisplayName, которая позволяет задать человекочитаемое имя для отображения в отчетах и IDE.

Без аннотации нужно придумывать длинные замысловатые названия для тестовых методов:

@Test
void getUserByIdWhenUserExists() {
// ... код теста
}

При этом в отчете будет выведено: getUserByIdWhenUserExists()

Если использовать @DisplayName, то можно не заморачиваться с именем теста и просто назвать его test1() так как суть будет отражена в отображаемом имени:

@Test
@DisplayName("""
  При получении пользователя по ID: Если пользователь существует,
  то возвращаются корректные данные.
""")
void test1() {
  // ... код теста
}

@Nested

Часто бывает, что в рамках одного тестового класса различные проверки можно объединить в смысловые группы, например при тестировании одного и того же метода с разными условиями и разным ожидаемым результатом . Для этого очень удобно использовать аннотацию @Nested.

Рассмотрим пример:

@DisplayName("Тесты сервиса работы с пользователями")
class UserServiceTest {

  @Nested
  @DisplayName("При создании пользователя:")
  class CreateUserTests {

    @Test
    @DisplayName("Если переданы корректные данные, возвращается созданный пользователь")
      void test1() {
      // ... тест успешного создания
    }

    @Test
    @DisplayName("Если email уже используется, будет брошено ValidationException")
    void test2() {
      // ... тест валидации email
    }
  }

  @Nested
  @DisplayName("При обновлении пользователя:")
  class UpdateUserTests {

    @Test
    @DisplayName("Если текущий пользователь имеет роль ROLE_ADMIN, редактирование проходит успешно")
    void test1() {
      // ... тест для админа
    }

    @Test
    @DisplayName("""
        Если текущий пользователь не имеет роль ROLE_ADMIN
        и редактируемый профиль ему не принадлежит,
        то будет брошено AccessDeniedException
      """)
    void test2() {
      // ... тест проверки прав
    }
  }

  @Nested
  @DisplayName("При удалении пользователя:")
  class DeleteUserTests {
    // ...различные проверки удаления
  }
}

При этом в отчете в CI или при запуске тестов в IDE мы получим структурированный человекочитаемый отчет, из которого сразу понятно, что именно поломалось:

Тесты сервиса работы с пользователями
├─ При создании пользователя:
│  ├─ Если переданы корректные данные, возвращается созданный пользователь
│  └─ Если email уже используется, будет брошено ValidationException
│
├─ При обновлении пользователя:
│  ├─ Если текущий пользователь имеет роль ROLE_ADMIN, редактирование проходит успешно
│  └─ Если текущий пользователь не имеет роль ROLE_ADMIN и редактируемый профиль ему не принадлежит, то будет брошено AccessDeniedException
│
└─ При удалении пользователя
   └─ ...

Преимущества подхода:

  • Логическая группировка Тесты разделены по функциональным блокам: создание, обновление, удаление.

  • Иерархическая структура Можно создавать многоуровневые группы с помощью вложенных @Nested классов.

  • Читаемые названия Сочетание @DisplayName делает отчеты визуально понятными.

  • Изоляция контекста Каждая группа имеет свою область видимости (можно использовать @BeforeEach внутри @Nested).

Emoji

Если не лень, и время позволяет, можно снабдить @DisplayName подходящими emoji, это еще больше повысит читаемость и задаст правильное настроение вашим тестам, а также разнообразит отчет о прохождении тестов:

@DisplayName("🧑 Тесты сервиса работы с пользователями")
class UserServiceTest {

  @Nested
  @DisplayName("➕ При создании пользователя:")
  class CreateUserTests {

    @Test
    @DisplayName("✅ Если переданы корректные данные, возвращается созданный пользователь")
    void test1() {
      // ... тест успешного создания
    }

    @Test
    @DisplayName("☠️ Если email уже используется, будет брошено ValidationException")
    void test2() {
      // ... тест валидации email
    }
  }
}
🧑Тесты сервиса работы с пользователями
├─➕ При создании пользователя:
│  ├─✅ Если переданы корректные данные, возвращается созданный пользователь
│  └─☠️ Если email уже используется, будет брошено ValidationException
│
...

Заключение

Эти нехитрые советы помогут сохранить вашу тестовую базу быстрой и легко поддающейся изменениям. Главное помнить, что чистота и эффективность тестового кода не менее важна чем аналогичные свойства прикладного кода.

А с какими проблемами в части тестирования spring-boot проекта сталкивались вы? Пожалуйста напишите в комментариях.

Теги:
Хабы:
-2
Комментарии15

Публикации

Работа

Java разработчик
198 вакансий

Ближайшие события