Основные принципы
Юнит-тесты — основа тестирования. Тестируйте изолированно отдельные компоненты.
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 проекта сталкивались вы? Пожалуйста напишите в комментариях.