Опирается на правила: TS-4TS-8 из Test Strategy Style Guide → раздел 2. BaseIntegrationTest.

Важно знать

  • Один платформенный PlatformBaseIntegrationTest + по одному доменному на каждый Bounded Context.
  • @ServiceConnection (Spring Boot 3.1+) — Spring сам прокидывает свойства в spring.datasource.*.
  • @TestInstance(PER_CLASS) — дорогой setup без static.
  • @MockitoBean DateTimeService + UuidGenerator — детерминированное время и UUID.
  • @Import(TestJwtConfiguration.class) — фейковый JWT + TestHttpHeaders.withSuccessToken().
  • Без @DynamicPropertySource для PostgreSQL — @ServiceConnection делает это сам.
  • Платформенный держит infrastructure, доменный добавляет DatabasePreparer.

BaseIntegrationTest — единая точка настройки. Без неё каждый тест-класс копирует Testcontainers, @MockitoBean, JWT-конфигурацию — через 50 тестов в проекте получаем 50 разных вариантов «как настроить». UCP формулирует двухуровневую схему: платформа + домен.

Платформенный base

TS-4: один на сервис.

@Import(TestJwtConfiguration.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("integration-test")
@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class PlatformBaseIntegrationTest {

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

    @MockitoBean
    protected DateTimeService dateTimeService;

    @MockitoBean
    protected UuidGenerator uuidGenerator;

    static {
        postgres.start();
    }
}

Что внутри:

  • @SpringBootTest(RANDOM_PORT) — полный context + случайный порт для HTTP.
  • @ActiveProfiles("integration-test") — профиль, отключающий Kafka/Redis.
  • @Testcontainers + @ServiceConnection — Spring сам поднимает PostgreSQL.
  • @TestInstance(PER_CLASS) — экземпляр класса живёт всё время, не нужно static для зависимостей.
  • @Import(TestJwtConfiguration.class) — единая JWT-конфигурация.
  • static { postgres.start(); } — контейнер запускается один раз на всю JVM (shared между тестами).

Доменный base

На каждый Bounded Context — свой:

public abstract class OrderBaseIntegrationTest extends PlatformBaseIntegrationTest {

    @Autowired
    protected OrderDatabasePreparer databasePreparer;

    @Autowired
    protected DSLContext dsl;

    @BeforeEach
    void clearDatabase() {
        databasePreparer.clearAll();
    }
}

Что добавляет:

  • @Autowired DatabasePreparer — fluent setup БД per BC.
  • @Autowired DSLContext — для assertions через JOOQ.
  • @BeforeEach — чистая БД перед каждым тестом.
public abstract class PaymentBaseIntegrationTest extends PlatformBaseIntegrationTest {
    @Autowired
    protected PaymentDatabasePreparer databasePreparer;
}

Тест:

public class CreateOrderEndpointIntegrationTest extends OrderBaseIntegrationTest {
    @Autowired private TestRestTemplate restTemplate;

    @Test
    void create_whenValid_returns201() {
        // ...
    }
}

Это даёт DRY (один BaseIntegrationTest на сервис) + домен-специфичные расширения.

@ServiceConnection

TS-5: Spring Boot 3.1+ feature.

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

Что делает:

  • Spring сам распознаёт container, прокидывает URL/username/password в spring.datasource.url/username/password.
  • Не нужно писать @DynamicPropertySource:
// УСТАРЕЛО — не нужно
@DynamicPropertySource
static void postgresProps(DynamicPropertyRegistry r) {
    r.add("spring.datasource.url", postgres::getJdbcUrl);
    r.add("spring.datasource.username", postgres::getUsername);
    r.add("spring.datasource.password", postgres::getPassword);
}

Для других контейнеров (Redis, Kafka — если очень нужно для отдельного теста) — тоже работает: @ServiceConnection распознаёт RedisContainer, KafkaContainer через Spring Boot autoconfigure.

@TestInstance(PER_CLASS)

TS-6: lifecycle экземпляра.

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class PlatformBaseIntegrationTest { ... }

По умолчанию JUnit Jupiter создаёт новый экземпляр test class для каждого тест-метода. С PER_CLASSодин экземпляр на весь class.

Что это даёт:

  • @BeforeAll без static — можно использовать field-injection.
  • Тяжёлый setup один раз — не на каждый метод.
  • Поля inheritable — доменный base может иметь @Autowired поля без статики.

Цена — нужно следить за state между тестами (тестовая БД чистится через @BeforeEach).

@MockitoBean для time и UUID

TS-7: детерминизм.

@MockitoBean
protected DateTimeService dateTimeService;

@MockitoBean
protected UuidGenerator uuidGenerator;

В production code сервиса:

@Component
public class DateTimeService {
    public OffsetDateTime getCurrentDateTimeInUTC() {
        return OffsetDateTime.now(ZoneOffset.UTC);
    }
}

@Component
public class UuidGenerator {
    public UUID generate() {
        return UUID.randomUUID();
    }
}

В Handler:

@UseCase
@RequiredArgsConstructor
public class CreateOrderHandler implements UseCaseHandler<CreateOrderCommand, Order> {

    private final OrderRepository orderRepository;
    private final DateTimeService dateTimeService;
    private final UuidGenerator uuidGenerator;

    @Override
    @Transactional
    public Order handle(CreateOrderCommand command) {
        var order = Order.create(
            uuidGenerator.generate(),
            command,
            dateTimeService.getCurrentDateTimeInUTC()
        );
        return orderRepository.save(order);
    }
}

В тесте — given:

var orderId = UUID.fromString("11111111-1111-1111-1111-111111111111");
var now = OffsetDateTime.parse("2026-05-26T10:00:00Z");

given(uuidGenerator.generate()).willReturn(orderId);
given(dateTimeService.getCurrentDateTimeInUTC()).willReturn(now);

Теперь Order.id всегда 11111111-..., createdAt всегда 2026-05-26T10:00:00Z. Assertions точные.

@MockitoBean — заменяет @MockBean в Spring Boot 3.4+. Использует Mockito, регистрируется в Spring context как primary bean.

@Import(TestJwtConfiguration)

TS-8: тестовая JWT-конфигурация.

@TestConfiguration
public class TestJwtConfiguration {

    @Bean
    @Primary
    public JwtDecoder testJwtDecoder() {
        return token -> {
            if (token.equals("test-success-token")) {
                return Jwt.withTokenValue(token)
                    .header("alg", "none")
                    .claim("sub", "test-user-id")
                    .claim("realm_access", Map.of("roles", List.of("customer")))
                    .build();
            }
            if (token.equals("test-admin-token")) {
                return Jwt.withTokenValue(token)
                    .header("alg", "none")
                    .claim("sub", "test-admin-id")
                    .claim("realm_access", Map.of("roles", List.of("admin")))
                    .build();
            }
            throw new JwtException("Invalid test token");
        };
    }
}

Helper:

public final class TestHttpHeaders {

    public static HttpHeaders withSuccessToken() {
        var headers = new HttpHeaders();
        headers.setBearerAuth("test-success-token");
        return headers;
    }

    public static HttpHeaders withAdminToken() {
        var headers = new HttpHeaders();
        headers.setBearerAuth("test-admin-token");
        return headers;
    }

    public static HttpHeaders withCustomerToken(String customerId) {
        // generates token with sub=customerId
    }
}

В тесте:

var response = restTemplate.exchange(
    "/v1/orders",
    HttpMethod.POST,
    new HttpEntity<>(request, TestHttpHeaders.withSuccessToken()),
    OrderResponse.class
);

Это даёт: единый source-of-truth по тестовой авторизации; никаких токенов руками в каждом тесте; легко менять (новая роль, новый claim) в одном месте.

Что запрещено

АнтипаттернПравилоЧто взамен
@DynamicPropertySource для PostgreSQLTS-5@ServiceConnection
@TestInstance(PER_METHOD) (default)TS-6PER_CLASS
Instant.now() напрямую в productionTS-7DateTimeService бин
UUID.randomUUID() напрямуюTS-7UuidGenerator бин
JWT-токены руками в каждом тестеTS-8TestHttpHeaders.* хелперы
Платформенный base в каждом BC отдельноTS-4один на сервис
@MockBean (deprecated)TS-7@MockitoBean
Отсутствие @ActiveProfiles("integration-test")TS-4явный профиль
Несколько контейнеров PostgreSQL в одном servceTS-5один shared контейнер через static

Куда дальше

  • Test Strategy → раздел 2. BaseIntegrationTest — нормативные формулировки.
  • Базовые правила — синхронность, AAA.
  • DatabasePreparer — что добавляется в доменный base.
  • TestObjectGenerator — fluent builders.
  • Один тест — TestRestTemplate + TestHttpHeaders.
  • Auth → JWT validation — TestJwtConfiguration как замена oauth2ResourceServer.