Опирается на правила:
TS-1…TS-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) |
| PostgreSQL | Testcontainers PostgreSQLContainer |
| Контроллер | через HTTP (TestRestTemplate) |
| Внешние HTTP | WireMock или @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:
- Time через
@MockitoBean DateTimeService—given(dateTimeService.getCurrentDateTimeInUTC()).willReturn(now). - UUID через
@MockitoBean UuidGenerator—given(uuidGenerator.generate()).willReturn(orderId). - Async-обработчики (
@TransactionalEventListener AFTER_COMMIT) — переводим вBEFORE_COMMITдля теста или вызываем явно. - 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_returns200payOrder_whenConfirmed_returns200shipOrder_whenPaid_returns200cancelOrder_whenConfirmed_returns200
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Awaitility.await() в integration-тесте | TS-2 | синхронный flow, @MockitoBean |
Thread.sleep(1000) | TS-2 | детерминированные моки |
Instant.now() в коде сервиса | TS-2 | DateTimeService бин |
UUID.randomUUID() в коде сервиса | TS-2 | UuidGenerator бин |
| Один тест на весь lifecycle | TS-3 | один тест = один сценарий |
| Тесты с Kafka в дефолтном integration suite | TS-1 | Outbox-проверка, Kafka отключён |
| Тесты с Redis | TS-1 | spring.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-тесты отдельно.