🏗️ Раздел 17 · Вопрос #25

Как тестировать микросервисы

Тестирование микросервисов сложнее монолита: приложение разбито на десятки независимых процессов, общающихся по ненадежной сети с внешними базами данных и брокерами сообщений.


🟢 Junior Level

Тестирование микросервисов сложнее монолита: приложение разбито на десятки независимых процессов, общающихся по ненадежной сети с внешними базами данных и брокерами сообщений.

Пирамида тестирования микросервисов:
                 / \
                /E2E\             ── 10% (Медленные сквозные тесты на Staging)
               /-----\
              /Contract\          ── 20% (Pact / Spring Cloud Contract)
             /-----------\
            / Integration \       ── 30% (Testcontainers: БД, Kafka, WireMock)
           /---------------\
          /      Unit       \     ── 40% (Быстрые изолированные тесты бизнес-логики)
         /-------------------\

Основные уровни тестирования:

  1. Unit-тесты (Модульные): Проверяют изолированные классы и методы без поднятия Spring-контекста и сети. Выполняются за миллисекунды.
  2. Интеграционные тесты (Integration): Проверяют взаимодействие сервиса с его адаптерами (базой данных, Kafka, Redis) в изолированном Docker-окружении (Testcontainers).
  3. Контрактные тесты (Contract Tests): Проверяют совместимость API между сервисом-потребителем и сервисом-поставщиком без запуска всей системы.
  4. Сквозные тесты (End-to-End / E2E): Проверяют полный пользовательский сценарий на развернутом стенде.

🟡 Middle Level

1. Интеграционное тестирование с Testcontainers и WireMock

Вместо нестабильных In-Memory заглушек (типа базы H2) современным стандартом является запуск реальных продакшен-версий зависимостей в Docker через Testcontainers:

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

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

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

    @DynamicPropertySource
    static void dynamicProperties(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);
    }

    @Autowired
    private OrderService orderService;

    @Test
    void shouldCreateOrderAndPublishKafkaEvent() {
        OrderRequest request = new OrderRequest("user-123", BigDecimal.valueOf(1500));
        
        OrderResponse response = orderService.createOrder(request);

        assertNotNull(response.orderId());
        assertEquals(OrderStatus.CREATED, response.status());
    }
}

2. Мокирование внешних HTTP-вызовов через WireMock

Если сервис вызывает внешний шлюз банка, вызов в реальный банк запрещен. Используется WireMock:

@AutoConfigureWireMock(port = 0) // Случайный свободный порт
class PaymentClientTest {

    @Test
    void shouldHandleBankTimeoutGracefully() {
        // Задаем поведение мока: ответить с задержкой 3 секунды
        stubFor(post(urlEqualTo("/api/v1/charge"))
            .willReturn(aResponse()
                .withStatus(200)
                .withFixedDelay(3000)));

        assertThrows(CallNotPermittedException.class, () -> {
            paymentGatewayClient.charge(new ChargeDto(100));
        });
    }
}

🔴 Senior Level

1. Consumer-Driven Contract Testing (Pact / Spring Cloud Contract)

Главная боль микросервисов: разработчики Payment Service переименовали поле userId в user_id, и при деплое упали 5 других сервисов. E2E-тесты находят это слишком поздно. Решение — Контрактное тестирование, управляемое потребителем (CDC):

1. Сервис-потребитель (Order Service) ──► Пишет тест-ожидание (Pact)
                                                 │
                                                 ▼ (Генерация pact.json)
2. Pact Broker (Реестр контрактов)   ◄── Публикация контракта в CI/CD
        │
        ▼ (Выкачивание контракта в CI провайдера)
3. Сервис-поставщик (Payment Service) ──► Прогоняет автотесты на соответствие контракту
                                          (Если контракт сломан -> Build Failed ❌)

Преимущество: Команда Payment Service в своем pull request мгновенно видит: «Ваш коммит ломает контракт сервиса Orders на 42-й строчке». Тесты выполняются за секунды в изоляции без подъема чужих сервисов.

2. Борьба с Flaky-тестами в асинхронных Event-Driven системах

При тестировании очередей Kafka/RabbitMQ использование Thread.sleep(2000) приводит к мигающим тестам (Flaky Tests): на быстром ноутбуке тест проходит, а в перегруженном CI на GitHub Actions падает по таймауту.

Решение: Awaitility (Поллинг состояния с предикатом):

@Test
void shouldProcessKafkaMessageAsynchronously() {
    kafkaTemplate.send("order-topic", new OrderCreatedEvent(42L));

    // Awaitility периодически опрашивает базу с экспоненциальной паузой
    await().atMost(5, TimeUnit.SECONDS)
           .pollInterval(100, TimeUnit.MILLISECONDS)
           .untilAsserted(() -> {
               Optional<Order> order = orderRepository.findById(42L);
               assertTrue(order.isPresent());
               assertEquals(OrderStatus.PROCESSED, order.get().getStatus());
           });
}

3. Антипаттерн: E2E Ice Cream Cone («Перевернутая пирамида»)

Когда компания не умеет писать контрактные тесты и Testcontainers, она скатывается в E2E-рожок мороженого:

  • Пишется 500 гигантских Selenium / Playwright / E2E тестов, требующих одновременного запуска 40 микросервисов в общем тестовом контуре (Staging).
  • Следствия:
    1. Тесты идут по 4 часа.
    2. Если в тестовом окружении моргнула сеть или упал сервис авторизации, краснеют все 500 тестов.
    3. Разработчики перестают верить тестам и мержат код с флагом skipTests.
  • Senior-решение: Сокращение E2E до 5–10 критических Smoke-сценариев («Пользователь авторизовался и оплатил корзину»). 90% проверок переносятся вниз на уровень Unit, Testcontainers и Pact.

4 Tricky Questions

1. Почему использование In-Memory базы H2 для интеграционных тестов репозиториев Spring Data JPA считается опасной практикой?

Ответ: H2 эмулирует синтаксис PostgreSQL/MySQL, но не совпадает с ними на уровне движка:

  1. Диалекты и типы данных: H2 не поддерживает нативные типы PostgreSQL: JSONB, диапазоны, сетевые типы INET, массивы UUID[].
  2. Специфичный SQL: Сложные запросы со специфичными операторами PostgreSQL (->>, ILIKE, оконные функции, ON CONFLICT DO UPDATE) падают в H2 с синтаксической ошибкой.
  3. Блокировки и транзакции: H2 по-другому обрабатывает уровни изоляции (REPEATABLE READ), не воспроизводит поведение дедлоков и блокировок SELECT ... FOR UPDATE SKIP LOCKED. Стандарт: Использование Testcontainers с официальным образом реальной СУБД (PostgreSQL 16) гарантирует 100% идентичность тестов продакшену.

2. Как запустить 50 интеграционных тестов с Testcontainers за 30 секунд вместо 5 минут?

Ответ: По умолчанию Testcontainers запускает и гасит Docker-контейнеры для каждого тестового класса заново, что отнимает минуты. Оптимизация: Singleton Containers Pattern:

  1. Объявляется общий базовый абстрактный класс AbstractIntegrationTest, в котором контейнеры объявляются как статические переменные без аннотации @Container и запускаются вручную в статическом блоке:
    static {
        postgres.start();
        kafka.start();
    }
    
  2. Все тесты наследуются от этого класса. Контейнеры поднимаются ровно один раз на весь запуск тестового сьюта и переиспользуются всеми классами, сокращая время тестирования на порядок.
  3. Очистка данных между тестами выполняется через @Sql скрипты очистки или библиотеку DatabaseRider.

3. Как проверить обратную совместимость схем данных Kafka (Schema Evolution) в тестах?

Ответ: Используется Confluent Schema Registry Testcontainers: В CI/CD пайплайн встраивается плагин Maven/Gradle: schema-registry:test-compatibility. Он сверяет новый файл схемы Avro/Protobuf с предыдущей версией из Git по правилам совместимости (BACKWARD, FULL). Тест падает, если разработчик удалил обязательное поле без дефолтного значения или изменил тип данных поля.

4. В чем разница между тестированием с замоканным Clock и использованием Thread.sleep() для проверки протухания TTL?

Ответ: Использование реального ожидания Thread.sleep(5000) в тестах замедляет билд и делает тесты нестабильными. Правильный подход: Внедрение Java java.time.Clock в виде Spring бина: В коде логики вызов Instant.now(clock) опирается на внедренный Clock. В тестах используется MutableClock или Clock.offset(), позволяя мгновенно перевести время приложения на 1 час вперед в рамках одного потока без реального ожидания:

TestClock.forward(Duration.ofHours(1));
assertTrue(sessionService.isExpired(sessionId));

🎯 Шпаргалка для интервью

Главное:

  • Пирамида тестирования: Unit (быстро, много) $\to$ Integration (Testcontainers) $\to$ Contract (Pact) $\to$ E2E (минимум).
  • Testcontainers — промышленный стандарт для интеграционных тестов: запуск реальных PostgreSQL, Kafka, Redis в Docker-контейнерах.
  • Используйте Singleton Containers Pattern для переиспользования одного контейнера между всеми тестами.
  • Consumer-Driven Contract Testing (Pact / Spring Cloud Contract): Потребитель задает контракт $\to$ поставщик проверяет его в своем CI. Исключает поломки API при раздельном деплое.
  • WireMock — для имитации внешних HTTP-сервисов и тестирования таймаутов/Circuit Breaker.
  • Awaitility — для стабильного тестирования асинхронных очередей Kafka без Thread.sleep().

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «Для интеграционных тестов с базой мы всегда используем H2 In-Memory базу данных» (H2 не поддерживает фичи PostgreSQL и скрывает баги SQL).
  • ❌ «Мы проверяем интеграцию микросервисов, запуская 50 сервисов вручную на Staging и гоняя E2E тесты» (Хрупкий E2E-рожок мороженого; заменяется Contract-тестами).
  • ❌ «Для проверки асинхронной логики в тестах мы ставим Thread.sleep(5000)» (Приводит к Flaky-тестам и замедлению сборки).

Связанные темы: