Опирается на правила: TS-26TS-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 (@Valid annotation 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 до DBIntegration
Validation @NotBlank email@WebMvcTest (быстрее)
@PreAuthorize hasRole('admin') returns 403@WebMvcTest
Outbox event creation after confirmIntegration
ABAC @access.canEditOrderIntegration (нужна 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 objects500-20005msНикаких
IntegrationUse case end-to-end в сервисе50-200100-500msDB + WireMock
E2ECross-system flows5-105-30sKafka + real services

Без unit — integration становится тяжёлым (тестируем simple Order logic через полный стек). Без integration — нет уверенности, что HTTP + DB + Outbox связаны правильно. Без E2E — не знаем, доставляется ли Kafka сообщение downstream.

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

АнтипаттернПравилоЧто взамен
Только integration tests, нет unitTS-26пирамида
Unit-тест с @SpringBootTestTS-26plain JUnit
Integration для тестирования validation @NotBlankTS-27@WebMvcTest (быстрее)
E2E без @Tag("e2e")TS-28tag для отделения от integration
E2E в обычном CI stepTS-28отдельный slow CI step
EmbeddedKafka в integration suiteTS-28только в @Tag("e2e")
> 20 E2E тестовTS-28≤ 10, остальное в integration через Outbox
Unit-тест моки Spring beansTS-26plain 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.