Опирается на правила: TS-1TS-3 из Test Strategy Style Guide → раздел 1. Базовые правила.

Важно знать

  • Тест должен быть быстрым и детерминированным. Awaitility/Thread.sleep — признак того, что это не тест бизнес-логики.
  • Интеграционный тест = полный Spring-контекст + реальный PostgreSQL + HTTP через TestRestTemplate.
  • Внешние HTTP — WireMock или @MockitoBean.
  • Kafka/Redis заменяются на Outbox-проверку или выключаются профилем.
  • Все тесты синхронные — никаких CompletableFuture.get(), Awaitility.await(), Thread.sleep.
  • Время и UUID детерминированные через @MockitoBean.
  • Один тест — один сценарий, AAA-структура.

UCP-подход к тестам построен вокруг одной идеи: тест должен быть быстрым (мс, не секунды), детерминированным (одинаковый результат каждый раз), простым (один сценарий). Awaitility, Thread.sleep, поднятый Kafka — признаки, что мы тестируем не бизнес-логику, а инфраструктурную интеграцию (которая нужна реже и пишется отдельно).

Что включает интеграционный тест

TS-1: полный стек, минус внешние системы.

ЧастьГде в тесте
Spring context@SpringBootTest(webEnvironment = RANDOM_PORT)
PostgreSQLTestcontainers PostgreSQLContainer
Контроллерчерез HTTP (TestRestTemplate)
Внешние HTTPWireMock или @MockitoBean
KafkaНЕ поднимаем; проверяем Outbox-таблицу
RedisНЕ поднимаем; spring.cache.type=none в профиле
Время@MockitoBean DateTimeService
UUID@MockitoBean UuidGenerator

Это даёт:

  • End-to-end внутри сервиса — от HTTP-запроса до строки в PostgreSQL.
  • Без асинхронных рассинхронизаций — нет ожидания Kafka consumer-а.
  • Время выполнения — один тест занимает 50-200ms (vs минуты при подъёме Kafka).
public class CreateOrderEndpointIntegrationTest extends OrderBaseIntegrationTest {

    @Autowired private TestRestTemplate restTemplate;
    @Autowired private OrderDatabasePreparer databasePreparer;

    @Test
    @DisplayName("BR-001: confirm order successfully creates outbox event")
    void confirmOrder_whenDraft_returns200AndCreatesOutbox() {
        // Arrange
        var orderId = UUID.randomUUID();
        var now = OffsetDateTime.now().withNano(0);
        given(uuidGenerator.generate()).willReturn(orderId);
        given(dateTimeService.getCurrentDateTimeInUTC()).willReturn(now);

        var draft = new OrderTestObjectGenerator()
            .withId(orderId)
            .withStatus(OrderStatus.DRAFT)
            .generate();
        databasePreparer.createOrder(draft).prepare();

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

        // Assert
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        var outbox = dsl.selectFrom(OUTBOX).fetch();
        assertThat(outbox).extracting(r -> r.getEventType())
            .containsExactly("OrderConfirmed");
    }
}

Все тесты синхронные

TS-2: нет Awaitility, Thread.sleep, CompletableFuture.get без timeout.

// КАТАСТРОФА — async-индетерминизм
Awaitility.await()
    .atMost(5, TimeUnit.SECONDS)
    .untilAsserted(() -> assertThat(orderRepository.findById(orderId)).isPresent());

// КАТАСТРОФА — тест зависит от planet rotation
Thread.sleep(1000);
assertThat(orderRepository.findById(orderId)).isPresent();

Что плохо:

  • Flaky — на медленном CI test fails, на быстром проходит → «retry test» становится практикой.
  • Медленно — каждый Awaitility — потеря секунд, тесты идут минутами.
  • Скрывает баги — async-bug «event запоздал на 50ms» проходит в Awaitility(5s), но в проде клиент уже timed out.

Корректно — синхронный flow:

  1. Time через @MockitoBean DateTimeServicegiven(dateTimeService.getCurrentDateTimeInUTC()).willReturn(now).
  2. UUID через @MockitoBean UuidGeneratorgiven(uuidGenerator.generate()).willReturn(orderId).
  3. Async-обработчики (@TransactionalEventListener AFTER_COMMIT) — переводим в BEFORE_COMMIT для теста или вызываем явно.
  4. Outbox-relay — не запускается в тесте. Outbox-таблица проверяется напрямую через DSLContext.

См. также Kafka, Redis, async — по умолчанию НЕТ.

AAA-структура

TS-3: один тест — один сценарий.

@Test
@DisplayName("BR-002: confirm fails when reservation failed")
void confirmOrder_whenReservationFailed_returns409() {
    // Arrange
    var orderId = UUID.randomUUID();
    given(uuidGenerator.generate()).willReturn(orderId);

    var draft = new OrderTestObjectGenerator()
        .withId(orderId)
        .withStatus(OrderStatus.RESERVATION_FAILED)
        .generate();
    databasePreparer.createOrder(draft).prepare();

    // Act
    var response = restTemplate.exchange(
        "/v1/orders/" + orderId + "/confirm",
        HttpMethod.POST,
        new HttpEntity<>(TestHttpHeaders.withSuccessToken()),
        ProblemDetailJsonBean.class
    );

    // Assert
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
    assertThat(response.getBody().getCode()).isEqualTo("OUT_OF_STOCK");
}

Три блока с пустой строкой между ними. Каждый блок имеет одну ответственность:

  • Arrange — подготовка БД, моки time/UUID.
  • Act — один HTTP-вызов.
  • Assert — проверки результата и побочных эффектов.

Не «проверить всё в одном»

// ПЛОХО — мега-тест проверяет 5 сценариев
@Test
void orderLifecycle() {
    // создать заказ
    // подтвердить
    // оплатить
    // отгрузить
    // отменить
}

Что плохо:

  • Если падает шаг 3 — не знаем, что не так с шагом 4-5.
  • Имя теста не описывает сценарий.
  • Невозможно запустить отдельный сценарий.

Корректно — отдельные тесты:

  • confirmOrder_whenDraft_returns200
  • payOrder_whenConfirmed_returns200
  • shipOrder_whenPaid_returns200
  • cancelOrder_whenConfirmed_returns200

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

АнтипаттернПравилоЧто взамен
Awaitility.await() в integration-тестеTS-2синхронный flow, @MockitoBean
Thread.sleep(1000)TS-2детерминированные моки
Instant.now() в коде сервисаTS-2DateTimeService бин
UUID.randomUUID() в коде сервисаTS-2UuidGenerator бин
Один тест на весь lifecycleTS-3один тест = один сценарий
Тесты с Kafka в дефолтном integration suiteTS-1Outbox-проверка, Kafka отключён
Тесты с RedisTS-1spring.cache.type=none
@MockBean (deprecated в Spring Boot 3.4)TS-2@MockitoBean
Длинное имя без @DisplayName с BR-кодомTS-3@DisplayName("BR-002: ...")

Куда дальше

  • Test Strategy → раздел 1. Базовые правила — нормативные формулировки.
  • BaseIntegrationTest — платформенный + доменный base.
  • DatabasePreparer — fluent setup БД.
  • TestObjectGenerator — fluent builders.
  • Один тест — структура теста.
  • Kafka, Redis, async — по умолчанию НЕТ — почему не поднимаем.
  • Что НЕ покрывается интеграционными — unit-тесты отдельно.