Как тестировать микросервисы
Тестирование микросервисов сложнее монолита: приложение разбито на десятки независимых процессов, общающихся по ненадежной сети с внешними базами данных и брокерами сообщений.
🟢 Junior Level
Тестирование микросервисов сложнее монолита: приложение разбито на десятки независимых процессов, общающихся по ненадежной сети с внешними базами данных и брокерами сообщений.
Пирамида тестирования микросервисов:
/ \
/E2E\ ── 10% (Медленные сквозные тесты на Staging)
/-----\
/Contract\ ── 20% (Pact / Spring Cloud Contract)
/-----------\
/ Integration \ ── 30% (Testcontainers: БД, Kafka, WireMock)
/---------------\
/ Unit \ ── 40% (Быстрые изолированные тесты бизнес-логики)
/-------------------\
Основные уровни тестирования:
- Unit-тесты (Модульные): Проверяют изолированные классы и методы без поднятия Spring-контекста и сети. Выполняются за миллисекунды.
- Интеграционные тесты (Integration): Проверяют взаимодействие сервиса с его адаптерами (базой данных, Kafka, Redis) в изолированном Docker-окружении (Testcontainers).
- Контрактные тесты (Contract Tests): Проверяют совместимость API между сервисом-потребителем и сервисом-поставщиком без запуска всей системы.
- Сквозные тесты (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).
- Следствия:
- Тесты идут по 4 часа.
- Если в тестовом окружении моргнула сеть или упал сервис авторизации, краснеют все 500 тестов.
- Разработчики перестают верить тестам и мержат код с флагом
skipTests.
- Senior-решение: Сокращение E2E до 5–10 критических Smoke-сценариев («Пользователь авторизовался и оплатил корзину»). 90% проверок переносятся вниз на уровень Unit, Testcontainers и Pact.
4 Tricky Questions
1. Почему использование In-Memory базы H2 для интеграционных тестов репозиториев Spring Data JPA считается опасной практикой?
Ответ: H2 эмулирует синтаксис PostgreSQL/MySQL, но не совпадает с ними на уровне движка:
- Диалекты и типы данных: H2 не поддерживает нативные типы PostgreSQL:
JSONB, диапазоны, сетевые типыINET, массивыUUID[]. - Специфичный SQL: Сложные запросы со специфичными операторами PostgreSQL (
->>,ILIKE, оконные функции,ON CONFLICT DO UPDATE) падают в H2 с синтаксической ошибкой. - Блокировки и транзакции: H2 по-другому обрабатывает уровни изоляции (
REPEATABLE READ), не воспроизводит поведение дедлоков и блокировокSELECT ... FOR UPDATE SKIP LOCKED. Стандарт: Использование Testcontainers с официальным образом реальной СУБД (PostgreSQL 16) гарантирует 100% идентичность тестов продакшену.
2. Как запустить 50 интеграционных тестов с Testcontainers за 30 секунд вместо 5 минут?
Ответ: По умолчанию Testcontainers запускает и гасит Docker-контейнеры для каждого тестового класса заново, что отнимает минуты. Оптимизация: Singleton Containers Pattern:
- Объявляется общий базовый абстрактный класс
AbstractIntegrationTest, в котором контейнеры объявляются как статические переменные без аннотации@Containerи запускаются вручную в статическом блоке:static { postgres.start(); kafka.start(); } - Все тесты наследуются от этого класса. Контейнеры поднимаются ровно один раз на весь запуск тестового сьюта и переиспользуются всеми классами, сокращая время тестирования на порядок.
- Очистка данных между тестами выполняется через
@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-тестам и замедлению сборки).