JavaRush /Курси /Модуль 5. Spring /Лекція 158: Тестування застосунку: Unit та інтеграційні т...

Лекція 158: Тестування застосунку: Unit та інтеграційні тести

Модуль 5. Spring
Рівень 25 , Лекція 7
Відкрита

Ми будемо писати два типи тестів:

  1. Unit-тести: перевіряють окремі методи і компоненти в ізоляції. Жодної роботи з базою даних чи зовнішніми сервісами. Використовуємо моки (Mockito) для симуляції залежностей.
  2. Інтеграційні тести: перевіряють роботу кількох компонентів разом. Наприклад, взаємодію контролерів з сервісами і базою даних.

Підготовка до тестування

Залежності для тестування

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));
    }
}

Що тут відбувається?

  1. Ми використовуємо анотацію @Mock, щоб створити "підробний" UserRepository.
  2. За допомогою @InjectMocks ми інжектимо цей мок в UserService.
  3. У тестах ми налаштовуємо поведінку репозиторія (when(...).thenReturn(...)), щоб створити контрольоване середовище.
  4. Переконуємось, що метод працює коректно і викидає виняток, якщо користувача не знайдено.

Інтеграційні тести

Тепер перейдемо до складніших випадків, коли потрібно перевірити взаємодію компонентів. Ми будемо використовувати реальну базу даних (наприклад, 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());
    }
}

Налагодження тестів і типові помилки

  1. Помилка конфігурації контексту: переконайтеся, що ваші конфігурації @SpringBootTest і моки коректно налаштовані.
  2. Ледача завантаження даних: іноді інтеграційні тести можуть падати через те, що зв'язки між сутностями завантажуються ледачо. Це виправляється налаштуванням явного завантаження (FetchType.EAGER) або використанням DTO.
  3. Повільні тести: якщо ваші тести виконуються повільно, розгляньте використання Testcontainers для локальної ізоляції тестів у контейнерах.

Автоматизація тестування

Інтегруйте тести в CI/CD-пайплайн. При кожному новому коміті ваші Unit та інтеграційні тести повинні запускатися, щоб запобігти потраплянню багів в основну гілку коду. Більшість інструментів (Jenkins, GitLab CI, GitHub Actions) легко інтегруються з Maven або Gradle.


# Приклад пайплайну GitLab CI
stages:
  - test

test:
  script:
    - ./mvnw test

Отак ви можете забезпечити впевненість у своєму застосунку перед деплоєм. Тепер ви знаєте, як писати і Unit, і інтеграційні тести. Ну що, готові до наступного кроку? Пора занурюватися в контейнеризацію і автоматизацію збірки! 🚀

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ