Ми будемо писати два типи тестів:
- 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, і інтеграційні тести. Ну що, готові до наступного кроку? Пора занурюватися в контейнеризацію і автоматизацію збірки! 🚀
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ