Gymterview
middle

Как организовать тестирование в современном Java-проекте?

Стратегия тестирования в 2026 году строится вокруг пирамиды: unit-тесты (много, быстрые), slice-тесты и интеграционные (средне), E2E и contract-тесты (мало), плюс автоматические архитектурные проверки через ArchUnit.

Пирамида тестирования

Пример
        /  E2E  \           <- Мало (Playwright, API tests)
       /  Интегр. \         <- Средне (Testcontainers, @SpringBootTest)
      / Slice-тесты \       <- Средне (@WebMvcTest, @DataJpaTest)
     /   Unit-тесты   \    <- Много (JUnit 5 + Mockito)
    /  Архитектурные    \   <- Автоматически (ArchUnit)

Unit-тест бизнес-логики

OrderServiceTest
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock OrderRepository orderRepository;
    @Mock PaymentGateway paymentGateway;
    @Mock OrderEventPublisher eventPublisher;
    @InjectMocks OrderService orderService;

    @Test
    void shouldCreateOrderWithCorrectTotal() {
        CreateOrderCommand command = new CreateOrderCommand(
            UUID.randomUUID(),
            List.of(
                new OrderItemCommand(UUID.randomUUID(), 2, new Money("29.99", "USD")),
                new OrderItemCommand(UUID.randomUUID(), 1, new Money("49.99", "USD"))
            ));
        when(orderRepository.save(any(Order.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        Order result = orderService.createOrder(command);

        assertThat(result.getStatus()).isEqualTo(OrderStatus.CREATED);
        assertThat(result.getTotalAmount()).isEqualByComparingTo(new BigDecimal("109.97"));
        verify(eventPublisher).publishOrderCreated(result);
        verify(paymentGateway, never()).charge(any(), any());
    }

    @ParameterizedTest
    @CsvSource({
        "CREATED,    CONFIRMED, true",
        "CONFIRMED,  SHIPPED,   true",
        "DELIVERED,  CANCELLED, false"
    })
    void shouldValidateStatusTransition(OrderStatus from, OrderStatus to, boolean allowed) {
        assertThat(from.canTransitionTo(to)).isEqualTo(allowed);
    }
}

Testcontainers для интеграционных тестов

Testcontainers запускает реальные зависимости (PostgreSQL, Kafka, Redis) в Docker-контейнерах:

IntegrationTestBase
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
abstract class IntegrationTestBase {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine");

    @Container
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.7.0"));

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
        .withExposedPorts(6379);

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
    }
}

Slice-тесты

Пример
// Тест контроллера (без запуска всего контекста)
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired MockMvc mockMvc;
    @MockBean CreateOrderUseCase createOrderUseCase;

    @Test
    void shouldReturn201WhenOrderCreated() throws Exception {
        when(createOrderUseCase.create(any())).thenReturn(order);

        mockMvc.perform(post("/api/v1/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{...}"))
            .andExpect(status().isCreated())
            .andExpect(header().exists("Location"));
    }
}

ArchUnit для архитектурных правил

Пример
@AnalyzeClasses(packages = "com.example.order")
class ArchitectureTest {

    @ArchTest
    static final ArchRule domainShouldNotDependOnAdapters =
        noClasses().that().resideInAPackage("..domain..")
            .should().dependOnClassesThat().resideInAPackage("..adapter..");

    @ArchTest
    static final ArchRule domainShouldNotDependOnSpring =
        noClasses().that().resideInAPackage("..domain..")
            .should().dependOnClassesThat().resideInAPackage("org.springframework..");
}

Частые ошибки

  • Тестирование на H2 вместо реальной БД: различия в SQL-диалектах приводят к пропущенным багам
  • Отсутствие тестов на ошибочные сценарии: тестируется только happy path
  • Mock всего подряд: unit-тест, который мокает всё, не тестирует ничего
  • Запуск @SpringBootTest для каждого теста — используйте @WebMvcTest/@DataJpaTest где возможно

На собеседовании: покажите знание пирамиды тестирования и объясните, когда какой тип используется. Ключевая фраза: “Testcontainers заменил H2 — тестируем на реальном PostgreSQL”. Упоминание ArchUnit для автоматической проверки архитектурных правил показывает зрелость. Частый вопрос: “Чем @WebMvcTest отличается от @SpringBootTest?” — первый загружает только web-слой, второй поднимает весь контекст.