← назад к разделу

Модульные тесты проверяют логику в изоляции, но не говорят, правильно ли приложение работает с базой данных, кешем или внешними сервисами. Для этого нужны интеграционные тесты — с реальными зависимостями.

Проблема H2 и моков

Самый простой способ протестировать репозиторий — подключить встроенную базу данных H2. Это быстро, не требует Docker, тесты запускаются везде. Но у подхода есть серьёзный изъян: H2 — не PostgreSQL.

Различия накапливаются незаметно: синтаксис SQL-функций, поведение при конфликтах уникальности, работа JSON-типов, оконные функции. Тест проходит на H2, а на продакшне падает — потому что диалекты отличаются.

То же с моками базы данных: мок проверяет, что метод был вызван с нужными аргументами, но не проверяет сам SQL-запрос, маппинг результата и поведение транзакции. Интеграционный тест поднимает тот же PostgreSQL, что работает в продакшне, и убирает целый класс ошибок.

@SpringBootTest: полный контекст

@SpringBootTest поднимает весь контекст приложения — точно так же, как при запуске. Это наиболее тяжёлый вариант теста, зато самый близкий к реальной работе.

@SpringBootTest
@AutoConfigureMockMvc
class OrderApiTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    void createsOrder() throws Exception {
        mockMvc.perform(post("/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                        {"productId": "abc", "quantity": 2}
                        """))
            .andExpect(status().isCreated());
    }
}

По умолчанию сервер не стартует — вместо этого используется MockMvc. Если нужен реальный HTTP-порт, добавьте webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT.

Полный контекст оправдан, когда важно проверить сквозной путь: HTTP → сервис → база данных → ответ. Для изолированной проверки одного слоя есть срезы.

Срезы: @DataJpaTest и @WebMvcTest

Срез (slice) — это урезанный контекст Spring, в котором поднимается только нужный слой. Остальные бины не загружаются, поэтому срез стартует значительно быстрее полного контекста.

@DataJpaTest поднимает только слой работы с данными: репозитории, EntityManager, транзакции. По умолчанию он подключает встроенную базу — но вместо H2 можно подключить Testcontainers (об этом ниже).

@DataJpaTest
class ProductRepositoryTest {

    @Autowired
    ProductRepository repository;

    @Test
    void findsActiveProducts() {
        var saved = repository.save(new Product("Widget", true));
        var found = repository.findAllActive();
        assertThat(found).contains(saved);
    }
}

@WebMvcTest поднимает только слой контроллеров: @Controller, @ControllerAdvice, фильтры, MockMvc. Сервисы и репозитории в этот контекст не входят — их нужно мокировать через @MockitoBean.

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockitoBean
    OrderService orderService;

    @Test
    void returnsBadRequestOnMissingBody() throws Exception {
        mockMvc.perform(post("/orders"))
            .andExpect(status().isBadRequest());
    }
}

Testcontainers: реальный PostgreSQL в Docker

Testcontainers — это Java-библиотека, которая запускает Docker-контейнеры прямо из кода теста. Контейнер стартует перед тестом, останавливается после. Никакой настройки окружения вручную — достаточно установленного Docker.

<!-- build.gradle или pom.xml -->
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:postgresql'
@SpringBootTest
@Testcontainers
class OrderRepositoryTest {

    @Container
    static PostgreSQLContainer<?> postgres =
            new PostgreSQLContainer<>("postgres:16-alpine");

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

    @Autowired
    OrderRepository repository;

    @Test
    void savesAndReadsOrder() {
        var order = repository.save(new Order(UUID.randomUUID(), "PENDING"));
        assertThat(repository.findById(order.id())).isPresent();
    }
}

@Container + static означает, что контейнер создаётся один раз на класс. @DynamicPropertySource передаёт URL, логин и пароль контейнера в контекст Spring до его старта.

@ServiceConnection: без ручной настройки URL

Spring Boot 3.1 ввёл @ServiceConnection — аннотацию, которая убирает шаблонный @DynamicPropertySource. Spring сам распознаёт тип контейнера и настраивает нужные свойства.

@SpringBootTest
@Testcontainers
class OrderRepositoryTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres =
            new PostgreSQLContainer<>("postgres:16-alpine");

    // @DynamicPropertySource больше не нужен

    @Autowired
    OrderRepository repository;
}

Короткая формула: @ServiceConnection = автоматическая настройка datasource, Redis, RabbitMQ и других поддерживаемых типов контейнеров.

Для Redis это работает точно так же:

@Container
@ServiceConnection
static GenericContainer<?> redis =
        new GenericContainer<>("redis:7-alpine").withExposedPorts(6379);

Когда полный контекст, а когда срез

СитуацияЧто использовать
Сквозной тест HTTP → база → ответ@SpringBootTest + Testcontainers
Проверить SQL-запросы репозитория@DataJpaTest + @ServiceConnection
Проверить валидацию и ответы контроллера@WebMvcTest + @MockitoBean
Проверить бизнес-логику сервисаМодульный тест, без Spring-контекста

Правило: поднимать ровно столько контекста, сколько нужно. Лишние бины замедляют старт и могут вносить неожиданные зависимости. Полный @SpringBootTest оправдан только для сквозных сценариев.

Если тестов с Testcontainers много, контейнер стоит выносить в общий базовый класс с @Container static — тогда Docker-образ поднимается один раз для всего набора тестов, а не отдельно под каждый класс.

Коротко

  • H2 и моки баз данных скрывают ошибки, которые видны только с реальным PostgreSQL.
  • @SpringBootTest поднимает весь контекст; срезы @DataJpaTest и @WebMvcTest — только нужный слой.
  • Testcontainers запускает Docker-контейнер прямо из теста — тот же образ, что в продакшне.
  • @ServiceConnection (Spring Boot 3.1+) убирает @DynamicPropertySource и сам настраивает datasource.
  • Выбирайте минимальный контекст: срез быстрее полного @SpringBootTest.
  • Общий базовый класс с static контейнером сокращает время сборки при большом числе тестов.

Что почитать дальше

  • Пирамида тестирования — как соотносятся модульные, интеграционные и end-to-end тесты.
  • Моки и внешние зависимости — когда мок уместен, а когда лучше реальный контейнер.
  • Тесты в Spring — @SpringBootTest и срезы подробнее в контексте Spring-экосистемы.
  • Стандарты тестирования — стайл-гайд по структуре и именованию тестов.