Як тестувати мікросервіси
Тестування мікросервісів значно складніше за моноліт: застосунок розділений на десятки автономних процесів, що комунікують ненадійною мережею із зовнішніми базами даних та броке...
🟢 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): Перевіряють сумісність протоколу між сервісом-споживачем та сервісом-постачальником без розгортання повної системи.
- Наскрізні тести (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 Tests) в асинхронних системах
Під час тестування черг Kafka/RabbitMQ прямий виклик Thread.sleep(2000) призводить до миготливих тестів (Flaky Tests): на швидкому комп’ютері розробника тест проходить, а на завантаженому CI в хмарі падає за таймаутом.
Рішення: 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. Чому використання бази даних H2 в пам’яті для інтеграційних тестів Spring Data JPA вважається небезпечною практикою?
Відповідь: H2 емулює базовий SQL-синтаксис, проте істотно відрізняється від реальних рушіїв 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.time.Clock як Spring-біна:
У бізнес-коді виклик Instant.now(clock) спирається на інжектований годинник. У тестах застосовується MutableClock або Clock.offset(), що дозволяє миттєво перевести внутрішній час застосунку на годину вперед в один рядок коду:
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 сервісів одночасно на стенді й проганяючи E2E тести» (Крихкий «ріжок морозива»; має замінюватися контрактними тестами).
- ❌ «Для перевірки асинхронної обробки ми ставимо
Thread.sleep(5000)» (Призводить до Flaky-тестів та повільних збірок).