Тестирование в Java: unit, integration, e2e — полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Тестирование в Java: от модульных до сквозных тестов

Качество кода — один из главных показателей профессионализма разработчика. В мире Java тестирование давно стало не просто хорошей практикой, а стандартом индустрии. Без надежных тестов сложно поддерживать большие проекты, внедрять новые фичи и уверенно рефакторить код. В этой статье мы разберем три ключевых уровня тестирования: unit (модульное), integration (интеграционное) и e2e (сквозное). Вы узнаете, чем они отличаются, как их правильно писать на Java и какие инструменты использовать.

Пирамида тестирования и почему это важно

Прежде чем углубляться в детали, стоит вспомнить концепцию пирамиды тестирования (Mike Cohn). Она визуализирует идеальное соотношение тестов разных уровней:

  • Unit-тесты (основание пирамиды) — самые быстрые, дешевые и многочисленные. Их должно быть около 70%.
  • Integration-тесты (середина) — проверяют взаимодействие компонентов. Их около 20%.
  • E2E-тесты (вершина) — самые медленные и дорогие, но критически важные для проверки пользовательских сценариев. Их около 10%.

Соблюдение этой пропорции позволяет получить максимальную уверенность в коде при минимальных затратах времени на прогон тестов.

1. Unit-тестирование в Java

Unit-тесты проверяют изолированно один «юнит» кода — как правило, метод или класс. Их главная цель — убедиться, что логика метода работает корректно, независимо от внешних зависимостей (базы данных, API, файловой системы). Для изоляции используются mock-объекты.

Основные инструменты

  • JUnit 5 — стандартный фреймворк для написания тестов.
  • Mockito — библиотека для создания mock-объектов и проверки взаимодействий.
  • AssertJ — библиотека для fluent-ассертов (повышает читаемость).

Пример unit-теста с JUnit 5 и Mockito

Допустим, у нас есть сервис UserService, который зависит от UserRepository:

public class UserService {
    private final UserRepository repository;
    
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
    
    public User findById(Long id) {
        return repository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User not found"));
    }
}

Тест для этого метода:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository repository;

    @InjectMocks
    private UserService userService;

    @Test
    void testFindById_UserExists() {
        // Given
        Long userId = 1L;
        User expectedUser = new User(userId, "Alice");
        when(repository.findById(userId)).thenReturn(Optional.of(expectedUser));

        // When
        User actualUser = userService.findById(userId);

        // Then
        assertNotNull(actualUser);
        assertEquals("Alice", actualUser.getName());
        verify(repository).findById(userId);
    }

    @Test
    void testFindById_UserNotFound() {
        // Given
        Long userId = 99L;
        when(repository.findById(userId)).thenReturn(Optional.empty());

        // When & Then
        assertThrows(UserNotFoundException.class, () -> userService.findById(userId));
    }
}

Best practices:

  • Тестируйте один сценарий за раз (один assert на тест — не догма, но к этому стоит стремиться).
  • Используйте понятные имена тестов (testFindById_UserExists).
  • Не тестируйте private-методы напрямую — тестируйте их через публичные методы.

2. Интеграционное тестирование в Java

Интеграционные тесты проверяют, как разные части системы работают вместе. В отличие от unit-тестов, здесь мы поднимаем реальные или близкие к реальным зависимости: базу данных, очереди сообщений, внешние сервисы (через тестовые контейнеры).

Инструменты для интеграционных тестов

  • Spring Boot Test (@SpringBootTest) — для тестирования Spring-контекста.
  • Testcontainers — позволяет запускать Docker-контейнеры (PostgreSQL, Redis и др.) прямо из тестов.
  • @DataJpaTest — для тестирования JPA-репозиториев.

Пример интеграционного теста с Spring Boot и Testcontainers

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void testSaveAndFindUser() {
        // Given
        User user = new User(null, "Bob");

        // When
        User savedUser = userRepository.save(user);
        User foundUser = userRepository.findById(savedUser.getId()).orElse(null);

        // Then
        assertThat(foundUser).isNotNull();
        assertThat(foundUser.getName()).isEqualTo("Bob");
    }
}

Важно: Интеграционные тесты медленнее unit-тестов, поэтому их стоит запускать отдельно (например, с Maven профилем integration-test).

3. E2E (End-to-End) тестирование

E2E-тесты (сквозные) имитируют

Блоги

Рекомендаций по книгам