Мы будем писать два типа тестов:
- Unit-тесты: проверяют отдельные методы и компоненты в изоляции. Никакой работы с базой данных или внешними сервисами. Используем моки (Mockito) для симуляции зависимостей.
- Интеграционные тесты: проверяют работу нескольких компонентов вместе. Например, взаимодействие контроллеров с сервисами и базой данных.
Подготовка к тестированию
Зависимости для тестирования
Spring Boot уже включает в себя всё необходимое для базового тестирования. Однако стоит убедиться, что в вашем pom.xml добавлены такие зависимости:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
Если вы используете Gradle, эквивалентные строки будут выглядеть так:
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.testcontainers:testcontainers'
После добавления зависимостей убедитесь, что проект успешно перезагружается.
Unit-тесты
Начнем с написания Unit-тестов для сервисного слоя.
Пример кода: тестирование сервиса
Предположим, у нас есть следующая бизнес-логика в нашем сервисе:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
}
Тестируем с использованием Mockito
Для тестирования данного класса нам нужно изолировать его от базы данных. Это достигается с помощью моков:
@ExtendWith(MockitoExtension.class) // Подключаем интеграцию с Mockito
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldReturnUserWhenFound() {
// Arrange
Long userId = 1L;
User mockUser = new User(userId, "John Doe", "john.doe@example.com");
when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); // "Мокаем" поведение репозитория
// Act
User result = userService.findUserById(userId);
// Assert
assertNotNull(result);
assertEquals("John Doe", result.getName());
verify(userRepository, times(1)).findById(userId); // Проверяем, что метод был вызван ровно один раз
}
@Test
void shouldThrowExceptionWhenUserNotFound() {
// Arrange
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// Act & Assert
assertThrows(UserNotFoundException.class, () -> userService.findUserById(userId));
}
}
Что здесь происходит?
- Мы используем аннотацию
@Mock, чтобы создать "поддельный" UserRepository. - С помощью
@InjectMocksмы внедряем этот мок в UserService. - В тестах мы настраиваем поведение репозитория (
when(...).thenReturn(...)), чтобы создать контролируемую среду. - Убедимся, что метод отрабатывает корректно и выбрасывает исключение, если пользователь не найден.
Интеграционные тесты
Теперь перейдем к более сложным случаям, когда нам нужно проверить взаимодействие компонентов. Мы будем использовать настоящую базу данных (например, H2) и тестировать работу контроллеров и сервисов.
Настройка интеграционных тестов
Для интеграционного тестирования контроллеров Spring Boot предоставляет удобный инструмент: @SpringBootTest. Он поднимает контекст приложения и позволяет тестировать взаимодействие слоев.
Пример тестируемого REST-контроллера:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.findUserById(id);
return ResponseEntity.ok(user);
}
}
Интеграционный тест контроллера
Пишем тест, который обращается напрямую к нашему API:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // Запускаем с веб-сервером
@AutoConfigureMockMvc // Настраиваем MockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldReturnUserWhenExists() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
@Test
void shouldReturnNotFoundWhenUserDoesNotExist() throws Exception {
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
}
Отладка тестов и типичные ошибки
- Ошибка конфигурации контекста: убедитесь, что ваши конфигурации
@SpringBootTestи моки корректно настроены. - Ленивая загрузка данных: иногда интеграционные тесты могут падать из-за того, что связи между сущностями загружаются лениво. Это исправляется настройкой явной загрузки (FetchType.EAGER) или использованием DTO.
- Медленные тесты: если ваши тесты выполняются медленно, рассмотрите использование Testcontainers для локальной изоляции тестов в контейнерах.
Автоматизация тестирования
Интегрируйте тесты в CI/CD-пайплайн. При каждом новом коммите ваши Unit и интеграционные тесты должны запускаться, чтобы предотвратить попадание багов в основную ветку кода. Большинство инструментов (Jenkins, GitLab CI, GitHub Actions) легко интегрируются с Maven или Gradle.
# Пример GitLab CI пайплайна
stages:
- test
test:
script:
- ./mvnw test
Вот так вы можете обеспечить уверенность в своём приложении перед его деплоем. Теперь вы знаете, как писать как Unit, так и интеграционные тесты. Ну что, готовы к следующему шагу? Пора погружаться в контейнеризацию и автоматизацию сборки! 🚀
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ