Основные принципы
Юнит-тесты — основа тестирования. Тестируйте изолированно отдельные компоненты.
Slice-тесты (@DataJpaTest, @WebMvcTest и т. д.) — используйте для тестирования слоёв приложения с частичным поднятием контекста.
Интеграционные тесты (@SpringBootTest) — только для критических сценариев, где требуется полный контекст Spring.
Целевое соотношение тестов

70% — юнит-тесты.
25% — slice-тесты.
5% — интеграционные тесты.
Интеграционные тесты — это "тяжёлая артиллерия". Чем меньше их в проекте, тем быстрее и стабильнее наши сборки.
Когда что использовать?
Юнит-тесты:
Сервисы
Утилиты
Валидаторы
Алгоритмы
Slice-тесты:
Контроллеры
Репозитории
REST-клиенты
Полный список доступных слайсов есть по ссылке: https://docs.spring.io/spring-boot/appendix/test-auto-configuration/slices.html
Интеграционные тесты:
Ну что тут сказать... Лучше их избегать 🙃
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 проекта сталкивались вы? Пожалуйста напишите в комментариях.
