Опирается на правила:
TS-26…TS-28из Test Strategy Style Guide → раздел 8. Что НЕ покрывается интеграционными тестами.
Важно знать
- Чистая бизнес-логика агрегата — unit-тест без Spring.
- Контроллер + JSON сериализация без БД —
@WebMvcTest+MockMvc.- E2E с реальной Kafka / внешними сервисами —
@Tag("e2e"), ≤ 5-10 тестов на сервис.- Пирамида: много unit → integration → мало E2E.
- Каждый слой имеет цель и стоимость, дополняют друг друга.
- Integration test не заменяет unit, и наоборот.
- Без unit-тестов integration becomes slow и фокусируется на инварианты, для которых не нужна БД.
Integration-тесты решают end-to-end внутри сервиса: HTTP → Handler → DB. Не каждая логика требует этого стека. Чистый аггрегат (без БД), контроллер (без бизнеса), реальная Kafka (отдельный E2E suite) — разные слои с разными правилами.
Unit-тест агрегата — без Spring
TS-26: чистая бизнес-логика.
class OrderTest {
@Test
@DisplayName("BR-001: confirm transitions DRAFT order to CONFIRMED")
void confirm_whenDraft_transitionsToConfirmed() {
// Arrange
var order = Order.create(
UUID.randomUUID(),
UUID.randomUUID(),
List.of(new OrderItem(UUID.randomUUID(), 2, new BigDecimal("50.00")))
);
// Act
order.confirm();
// Assert
assertThat(order.status()).isEqualTo(OrderStatus.CONFIRMED);
}
@Test
@DisplayName("BR-002: cannot confirm cancelled order")
void confirm_whenCancelled_throwsIllegalStateException() {
// Arrange
var order = Order.create(...);
order.cancel("test reason");
// Act + Assert
assertThatThrownBy(() -> order.confirm())
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("CANCELLED");
}
@Test
@DisplayName("BR-003: total amount calculated as sum of items")
void totalAmount_whenMultipleItems_returnsSum() {
var order = Order.create(
UUID.randomUUID(),
UUID.randomUUID(),
List.of(
new OrderItem(UUID.randomUUID(), 2, new BigDecimal("50.00")),
new OrderItem(UUID.randomUUID(), 1, new BigDecimal("100.00"))
)
);
assertThat(order.totalAmount()).isEqualByComparingTo(new BigDecimal("200.00"));
}
}
Свойства:
- Без Spring context — обычный JUnit, никаких
@SpringBootTest. - Без БД — никаких Testcontainers.
- Без HTTP — никаких контроллеров.
- Без моков —
new Order(...)напрямую.
Скорость: ~5ms на тест. На два порядка быстрее integration.
Что тестируют:
- Инварианты агрегата (
Order.confirm() throws if cancelled). - Value objects (
Money.add(),Money.compareTo()). - Pure functions (calculation, validation).
Эти тесты — самые многочисленные. На один integration-тест приходится 10-20 unit-тестов.
@WebMvcTest для контроллеров
TS-27: контроллер + JSON, без БД.
@WebMvcTest(OrderController.class)
@Import({TestJwtConfiguration.class, ValidationConfiguration.class})
class OrderControllerTest {
@Autowired private MockMvc mockMvc;
@MockitoBean private UseCaseDispatcher dispatcher;
@Test
@DisplayName("BR-004: validation fails for missing customerId")
void create_whenMissingCustomerId_returns400() throws Exception {
mockMvc.perform(post("/v1/orders")
.header("Authorization", "Bearer test-success-token")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"items": [{"productId": "...", "quantity": 1}]
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
.andExpect(jsonPath("$.errors[0].field").value("customerId"));
}
@Test
@DisplayName("BR-005: returns 403 when customer role missing")
void create_whenNoCustomerRole_returns403() throws Exception {
mockMvc.perform(post("/v1/orders")
.header("Authorization", "Bearer test-no-role-token")
.contentType(MediaType.APPLICATION_JSON)
.content("{...}"))
.andExpect(status().isForbidden());
}
}
Что тестируют:
- HTTP request/response сериализация.
- Jakarta Validation (
@Validannotation behavior). - Security (
@PreAuthorizeповедение). - ProblemDetail mapping.
Что не тестируют:
- Бизнес-логику (mocked via
UseCaseDispatcher). - БД (no Testcontainers).
- Внешние HTTP (no WireMock).
Скорость: ~50ms (Spring context частичный, без БД).
Используем когда:
- Тестируем validation rules.
- Тестируем authorization rules (
@PreAuthorize). - Тестируем response shape (JSON структура).
Когда integration vs @WebMvcTest:
| Cценарий | Tool |
|---|---|
| End-to-end create order from HTTP до DB | Integration |
Validation @NotBlank email | @WebMvcTest (быстрее) |
@PreAuthorize hasRole('admin') returns 403 | @WebMvcTest |
| Outbox event creation after confirm | Integration |
ABAC @access.canEditOrder | Integration (нужна DB) |
E2E через @Tag("e2e")
TS-28: отдельный suite, ≤ 5-10 тестов.
@Tag("e2e")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("e2e")
public class OrderE2ETest {
@Container
static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Test
@DisplayName("E2E: order create flow with real Kafka")
void createOrderE2E() {
// отправляем HTTP create
// ждём в Kafka сообщение OrderCreated
// проверяем downstream сервис получил
}
}
CI:
# Обычный CI step
- run: ./gradlew test # без @Tag("e2e"), быстро
# Отдельный E2E step (раз в час, на main, before deploy)
- run: ./gradlew test -PincludeTags=e2e
Цель — не дублировать integration tests, а проверить cross-system flows:
- Real Kafka producer → consumer.
- Distributed tracing.
- Real S2S communication.
5-10 тестов на сервис. Не больше — иначе CI становится часами.
Пирамида тестов
▲
E2E
/---\
/ 5-10 \
/-------\
Integration
/ 50-200 \
/-------------\
/ Unit \
/ 500-2000 \
/-------------------\
| Слой | Цель | Количество | Скорость | Зависимости |
|---|---|---|---|---|
| Unit | Инварианты, value objects | 500-2000 | 5ms | Никаких |
| Integration | Use case end-to-end в сервисе | 50-200 | 100-500ms | DB + WireMock |
| E2E | Cross-system flows | 5-10 | 5-30s | Kafka + real services |
Без unit — integration становится тяжёлым (тестируем simple Order logic через полный стек). Без integration — нет уверенности, что HTTP + DB + Outbox связаны правильно. Без E2E — не знаем, доставляется ли Kafka сообщение downstream.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| Только integration tests, нет unit | TS-26 | пирамида |
Unit-тест с @SpringBootTest | TS-26 | plain JUnit |
Integration для тестирования validation @NotBlank | TS-27 | @WebMvcTest (быстрее) |
E2E без @Tag("e2e") | TS-28 | tag для отделения от integration |
| E2E в обычном CI step | TS-28 | отдельный slow CI step |
EmbeddedKafka в integration suite | TS-28 | только в @Tag("e2e") |
| > 20 E2E тестов | TS-28 | ≤ 10, остальное в integration через Outbox |
| Unit-тест моки Spring beans | TS-26 | plain Java, никаких моков |
Куда дальше
- Test Strategy → раздел 8. Что НЕ покрывается — нормативные формулировки.
- Базовые правила — что есть integration test.
- Один тест — пример integration теста.
- Kafka, Redis, async — НЕТ — почему Kafka не в integration.
- Внешние HTTP — WireMock — WireMock vs real service.
- DDD → aggregate — unit-тесты для инвариантов.
- Validation → where to validate —
@WebMvcTestдля validation.