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

Лекция 158: Тестирование приложения: Unit и интеграционные тесты

Модуль 5. Spring
16 уровень , 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, так и интеграционные тесты. Ну что, готовы к следующему шагу? Пора погружаться в контейнеризацию и автоматизацию сборки! 🚀

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ