В мире синхронных REST API тестирование сводится к простой формуле: отправил запрос — получил ответ — проверил код состояния. Но в событийно-управляемой архитектуре (EDA) этот подход разбивается о реальность. Здесь нет прямого ответа. Событие улетает в брокер, обрабатывается асинхронно, изменяет чьё-то состояние и порождает новые события. Как убедиться, что система работает правильно, если результат её работы растянут во времени?
К счастью, микросервисная архитектура обладает высокой модульностью. Если синхронные монолиты требовали развёртывания «полотна» зависимостей для одного теста, то событийные микросервисы позволяют изолировать логику. В этой главе мы разберём многоуровневую стратегию тестирования EDA: от чистых функций до полноценных клонированных кластеров.
Это пятнадцатая глава цикла. Она опирается на главу III (schema testing, совместимость), главу V (TopologyTestDriver, state stores), главу VI (watermarks, event time), главу VII (state store testing, Changelog), главу VIII (тестирование компенсаций), главу X (BPC), главу XI (MiniCluster, TestHarness), главу XII (EmbeddedKafka), главу XIII (contract testing) и главу XIV (testing infrastructure). Здесь мы замыкаем цикл: от паттернов и архитектуры — к качеству.
Классическая пирамида тестирования (unit → integration → E2E) в EDA имеет свою специфику. Ключевое отличие — contract testing становится отдельным слоем, параллельным пирамиде.
| Уровень | Доля | Скорость | Что проверяет |
|---|---|---|---|
| Unit | ~60% | Миллисекунды | Функции без состояния, топология, property-based |
| Integration | ~30% | Секунды–минуты | Testcontainers, rebalance, failure, state store |
| Contract | параллельно | Секунды | Schema testing, Pact, snapshot testing |
| E2E | ~10% | Минуты–часы | Эфемерные кластеры, chaos, load, replay |
Чем ниже уровень — тем больше тестов и тем они быстрее. Чем выше — тем меньше тестов и тем они дороже. Для EDA это правило работает точно так же, но с одним отличием: contract testing становится отдельным слоем, потому что контракты между производителем и потребителем — критичны, но не вписываются в классическую пирамиду.
Событийно-управляемая топология — это конвейер функций: filter, map, aggregate, join. Первый шаг — протестировать эти функции изолированно.
Трансформации, фильтрация и маршрутизация. Они не помнят прошлого. Тестировать тривиально: подали массив событий — проверили массив на выходе. Граничные условия, null, невалидные схемы — главные мишени.
Агрегации и оконные вычисления. Здесь понадобится mock хранилища состояний (Key-Value store). Эмулируем память сервиса, чтобы проверить, как функция накапливает и схлопывает данные во времени.
Что если ошибка не в функции map, а в том, как фреймворк маршрутизирует события между groupByKey и reduce? Писать собственный эмулятор брокера — плохая идея. Используйте встроенные драйверы.
// TopologyTestDriver для Kafka Streams
TopologyTestDriver driver = new TopologyTestDriver(topology, config);
TestInputTopic<String, OrderEvent> input = driver.createInputTopic(
"orders",
new StringSerializer(),
new OrderEventSerializer());
TestOutputTopic<String, Long> output = driver.createOutputTopic(
"customer-totals",
new StringDeserializer(),
new LongDeserializer());
input.pipeInput("order-1", new OrderEvent("cust-42", 150.0));
assertThat(output.readValueToMap()).containsEntry("cust-42", 1L);
driver.close();
Драйвер работает в том же JVM, эмулирует время, watermarks, repartition — без реального брокера. Работает за миллисекунды.
TopologyTestDriver уже упоминался в главе XII (лёгкие фреймворки). Здесь мы показываем его в контексте уровня 1 тестирования: unit-тест топологии, а не интеграционный тест.
Классический unit-тест проверяет конкретные примеры: «подал X — получил Y». Property-based test проверяет инварианты: «для любых X верно Y».
Генерирует случайные события, проверяет инварианты. При нахождении контрпримера — минимизирует его до простейшего.
Аналог для JVM. Генерация случайных событий, проверка свойств, shrink до минимального контрпримера.
Идемпотентность обработки, коммутативность агрегаций, монотонность счётчиков, отсутствие потерь при map/filter.
Подача случайных байтов вместо валидных событий. Проверка, что десериализатор не падает, а обработчик отдаёт ошибку в DLQ.
// Пример property-based test на jqwik
@Property
void total_is_monotonic(@ForAll @Positive BigDecimal amount) {
BigDecimal before = state.total();
state.apply(new DepositEvent(amount));
BigDecimal after = state.total();
assertThat(after).isGreaterThanOrEqualTo(before);
}
Property-based testing ловит баги, которые никогда не придут в голову автору примеров: редкие комбинации, граничные случаи, негативные значения.
В главе VI мы обсуждали watermarks, late events, grace period. Как их тестировать? TopologyTestDriver позволяет продвигать event time вручную.
// Продвигаем время и проверяем закрытие окна
input.pipeInput("key-1", event1, Instant.ofEpochSecond(100));
input.pipeInput("key-1", event2, Instant.ofEpochSecond(200));
// Продвигаем watermark
driver.advanceWallClockTime(Duration.ofSeconds(300));
// Проверяем, что окно закрылось и результат появился
assertThat(output.readValueToMap()).containsEntry("key-1", 2L);
// Подаём late event ПОСЛЕ закрытия окна
input.pipeInput("key-1", lateEvent, Instant.ofEpochSecond(150));
// Проверяем, что late event обработан или отброшен согласно grace period
// ...
Что тестировать:
Тестирование watermarks — это проверка того, что event time semantics работают правильно. Без этого теста вы не узнаете, что окна закрываются неверно, пока не увидите неправильные метрики в production.
State store — ключевой элемент stateful-обработки. Его надо тестировать отдельно:
Тестирование операторов в изоляции. Подача событий, проверка выхода, проверка state.
Для Kafka Streams Processor API. Тестирование processor'а без топологии.
После обработки событий — проверить, что state содержит ожидаемые значения. TopologyTestDriver даёт доступ к state stores.
Проверка, что punctuator очищает устаревшие записи. Проверка, что tombstone пишется в changelog.
// Проверка state store после обработки
KeyValueStore<String, Long> store = driver.getKeyValueStore("customer-totals");
assertThat(store.get("cust-42")).isEqualTo(3L);
// Проверка TTL cleanup
input.pipeInput("key-1", event1);
driver.advanceWallClockTime(Duration.ofHours(25));
driver.runScheduledPunctuators();
assertThat(store.get("key-1")).isNull();
Что тестировать:
В EDA схема — это закон. Если производитель молча изменит тип поля или удалит обязательный атрибут, потребитель упадёт. Тестирование эволюции схем должно быть встроено в CI/CD.
| Режим | Что проверяет | Когда использовать |
|---|---|---|
| Backward | Новый потребитель читает старые события | Потребители обновляются первыми |
| Forward | Старый потребитель читает новые события | Производители обновляются первыми |
| Full | Оба направления | Независимый деплой |
| Backward Transitive | Совместимость со всеми предыдущими версиями | Event Sourcing, долгое хранение |
# В CI-пайплайне перед деплоем
- name: Validate schema compatibility
run: |
curl -X POST $SCHEMA_REGISTRY_URL/compatibility/subjects/orders-value/versions/latest \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d @new-schema.json
# Если incompatible — CI fails, деплой блокируется
Schema Registry — enforcement-механизм. Без CI-проверки правила совместимости — это просто надежда. С проверкой — гарантия, что breaking change не проскочит в production.
Дополнение к schema testing — snapshot testing:
Это простой и эффективный подход: вы явно видите, что меняется, и осознанно принимаете решение.
Когда API генерирует команды и события (глава XIII), контракт между API и event schema должен быть протестирован.
Consumer-driven contracts. Потребитель описывает ожидания, продюсер проверяет в CI. Прямая связь с главой III (Schema Registry).
Для Java-экосистемы. Groovy DSL для описания контрактов, автоматическая генерация тестов.
API Gateway валидирует команды против JSON Schema перед публикацией в Kafka.
Testcontainers для полного pipeline: API → Command → Event → Read model.
Модульные тесты не проверят, как микросервис ведёт себя при сбое брокера, как происходит rebalance и как восстанавливается состояние из changelog. Для этого нужна локальная интеграционная среда.
Золотой стандарт — Testcontainers: реальный Kafka, Schema Registry, БД в Docker. Тест поднимает контейнеры, прогоняет события, проверяет итоговое состояние.
// Testcontainers + Kafka
@Container
KafkaContainer<?> kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));
@Test
void should_rebalance_and_resume() {
// 1. Запускаем consumer 1
// 2. Обрабатываем часть событий
// 3. Убиваем consumer 1
// 4. Запускаем consumer 2 — он должен подхватить партиции
// 5. Проверяем, что все события обработаны
}
Что тестировать:
Что делать, если вы используете Kinesis, Pub/Sub или Event Hubs, у которых нет локальных open-source аналогов?
| Платформа | Локальный эмулятор | Ограничения |
|---|---|---|
| AWS Kinesis | LocalStack | Не все фичи, latency отличается |
| Google Pub/Sub | Официальный эмулятор | Ограниченная поддержка ordering |
| Azure Event Hubs | Нет полноценного эмулятора | Тестирование против dev-сред |
Если у платформы есть локальный эмулятор — используйте его в CI. Если нет — используйте удалённые dev-среды с тщательной настройкой ACL и изоляцией по namespace. Но помните: это дороже и медленнее.
Когда нужно проверить производительность, нагрузку и восстановление после катастроф, локального Docker не хватит. Нужен полноценный кластер.
Копирование реальных топиков в тестовый кластер.
Плюсы: идеальное отражение реальности, учёт аномалий.
Минусы: риск утечки PII, нагрузка на прод, долгая репликация.
Генерация событий на основе схем из Schema Registry.
Плюсы: безопасно, быстро, можно создать редкие граничные случаи.
Минусы: не повторяет реальные паттерны и «грязь» прода.
Использование одного общего кластера для всех команд превращает его в «помойку». Тестовые топики с суффиксами -test-v2-final пересекаются с чужими тестами, данные устаревают, нагрузка от одной команды ломает тесты другой. Это «трагедия общин», которой нужно избегать.
Если организация инвестирует в инфраструктуру как код (IaC), лучший подход — программное создание временного кластера под каждый крупный тест:
Дорого, но это единственный способ гарантировать изоляцию и чистоту эксперимента.
EDA — распределённая система. Она должна выживать при сбоях. Chaos testing проверяет это системно.
Убивает случайные pod'ы в Kubernetes. Проверка, что сервис восстанавливается.
Chaos-эксперименты для Kubernetes. Готовые сценарии: pod-delete, network-loss, disk-fill.
Эмуляция сетевых сбоев: задержка, разрыв, потери пакетов. Проверка, что retry работает.
Managed chaos. Сценарии с гарантией безопасности. Для организаций без собственной chaos-платформы.
Что проверять:
| Инструмент | Язык | Особенность |
|---|---|---|
| k6 | JavaScript | Modern, встроенная метрика, cloud-версия |
| Gatling | Scala DSL | Мощный DSL, отчёты из коробки |
| JMeter | Java + UI | Классика, много плагинов, UI |
| Locust | Python | Простой DSL, распределённая нагрузка |
Особенность EDA: нужно генерировать миллионы событий в секунду. Не все инструменты справляются. Иногда проще написать свой генератор на Kafka Producer API, который пишет в топик с заданной скоростью.
Reprocessing — суперсила EDA (глава VI). Как тестировать?
Replay testing — единственный способ проверить, что новая версия обработки выдаёт те же результаты, что старая. Особенно важно для финансовых систем, где каждая цифра должна совпадать.
В главе VIII мы обсуждали Saga, компенсации, semantic locks. Как их тестировать?
Все шаги саги выполняются успешно. Проверка, что финальное состояние корректно.
Сбой на шаге N — компенсации выполняются для шагов 1..N-1. Проверка, что состояние откатилось.
Повторный вызов шага — результат тот же. Проверка через дедупликацию.
Зависшая сага — cleanup job снимает locks. Проверка через симуляцию таймаута.
// Temporal: тестирование компенсации
@Test
void should_compensate_when_payment_fails() {
// 1. Запустить workflow с mock activities
// 2. Симулировать сбой платёжного шага
// 3. Проверить, что вызваны компенсации в обратном порядке
// 4. Проверить, что финальное состояние консистентно
}
Иногда единственный способ проверить, как микросервис поведёт себя под реальным давлением — запустить его в продакшене. Звучит страшно, но в EDA это делается безопасно:
consumer group. Он читает данные, но не влияет на работу старых потребителей.Это позволяет провести smoke test на реальных данных, проверить задержки и масштабирование, не рискуя сломать бизнес-процессы.
Тестирование в EDA стоит денег:
| Уровень | Стоимость | Когда оправдано |
|---|---|---|
| Unit + Property | Практически $0 | Всегда |
| Testcontainers | CPU/RAM локально или в CI | Каждый PR |
| Schema testing | Практически $0 | Каждый PR |
| Contract testing | Практически $0 | Каждый PR |
| Эфемерные кластеры | $$–$$$ | Перед релизом |
| Chaos testing | $$ | Раз в месяц |
| Load testing | $$–$$$ | Перед major релизом |
| Production testing | Оплата инфраструктуры + риск | Для критичных сервисов |
Плохой DX = тесты не пишутся. Что важно:
Если тест пишется дольше, чем код, — тест не будет написан. Инвестируйте в инфраструктуру тестирования так же, как в production-инфраструктуру.
| Глава | Фокус | Связь с текущей главой |
|---|---|---|
| III. Идеальное событие | Schema Registry, эволюция | Schema testing, snapshot testing |
| V. Потоковая обработка | TopologyTestDriver, state stores | Уровень 1 тестирования |
| VI. Время в потоках | Watermarks, event time | Тестирование watermarks, late events |
| VII. Память микросервисов | State store testing | Тестирование stateful processors |
| VIII. Saga | Компенсации, human-in-the-loop | Тестирование Saga и компенсаций |
| X. BPC | Простой poll-process-produce | Базовый уровень тестирования |
| XI. Тяжёлые фреймворки | MiniCluster, TestHarness | Тестирование Flink jobs |
| XII. Лёгкие фреймворки | TopologyTestDriver, EmbeddedKafka | Тестирование Kafka Streams |
| XIII. API и UI | Contract testing | Testing между API и events |
| XIV. Налог на микросервисы | Testing infrastructure | Testing как часть платформы |
| XV. Как проверить «ненаблюдаемое» | Testing EDA | QA-перспектива всего цикла |
Не существует единственного «правильного» способа тестировать событийно-управляемые микросервисы. Модульность микросервисов даёт вам свободу выбирать инструмент под задачу. Инвестируйте в автоматизацию создания тестовых сред и генерации данных, и ваш QA-процесс перестанет быть «бутылочным горлышком», превратившись в надёжный фундамент для непрерывной доставки.
Глава XV цикла «Событийно-управляемая архитектура». Источники: Apache Kafka, Confluent, Apache Flink, Testcontainers, Pact, Temporal, LitmusChaos, k6, jqwik, Hypothesis, практические кейсы production-систем, FinCodeCow experience.
In the world of synchronous REST APIs, testing boils down to a simple formula: send request — receive response — check status code. But in Event-Driven Architecture (EDA), this approach breaks against reality. There is no direct response. The event flies to the broker, is processed asynchronously, changes someone's state, and generates new events. How do you ensure the system works correctly if the result is stretched over time?
Fortunately, microservice architecture has high modularity. Where synchronous monoliths required deploying a "sheet" of dependencies for a single test, event-driven microservices allow isolating logic. In this chapter, we'll explore a multi-level EDA testing strategy: from pure functions to fully cloned clusters.
This is the fifteenth chapter of the series. It builds on Chapter III (schema testing, compatibility), Chapter V (TopologyTestDriver, state stores), Chapter VI (watermarks, event time), Chapter VII (state store testing, Changelog), Chapter VIII (testing compensations), Chapter X (BPC), Chapter XI (MiniCluster, TestHarness), Chapter XII (EmbeddedKafka), Chapter XIII (contract testing), and Chapter XIV (testing infrastructure). Here we close the cycle: from patterns and architecture to quality.
| Level | Share | Speed | What It Verifies |
|---|---|---|---|
| Unit | ~60% | Milliseconds | Stateless functions, topology, property-based |
| Integration | ~30% | Seconds–minutes | Testcontainers, rebalance, failure, state store |
| Contract | parallel | Seconds | Schema testing, Pact, snapshot testing |
| E2E | ~10% | Minutes–hours | Ephemeral clusters, chaos, load, replay |
The lower the level — the more tests and the faster they run. The higher — the fewer tests and the more expensive they are. For EDA this works the same, but with one difference: contract testing becomes a separate layer because contracts between producer and consumer are critical but don't fit into the classic pyramid.
Transformations, filtering, routing. They don't remember the past. Testing is trivial: fed an array of events — checked the output array. Boundary conditions, null, invalid schemas — main targets.
Aggregations and window computations. Here you need a mock of the state store (Key-Value store). Emulate the service's memory to verify how the function accumulates and collapses data over time.
// TopologyTestDriver for Kafka Streams
TopologyTestDriver driver = new TopologyTestDriver(topology, config);
TestInputTopic<String, OrderEvent> input = driver.createInputTopic(
"orders", new StringSerializer(), new OrderEventSerializer());
TestOutputTopic<String, Long> output = driver.createOutputTopic(
"customer-totals", new StringDeserializer(), new LongDeserializer());
input.pipeInput("order-1", new OrderEvent("cust-42", 150.0));
assertThat(output.readValueToMap()).containsEntry("cust-42", 1L);
driver.close();
A classic unit test verifies concrete examples. A property-based test verifies invariants: "for any X, Y holds".
// Example of a property-based test with jqwik
@Property
void total_is_monotonic(@ForAll @Positive BigDecimal amount) {
BigDecimal before = state.total();
state.apply(new DepositEvent(amount));
BigDecimal after = state.total();
assertThat(after).isGreaterThanOrEqualTo(before);
}
// Advance time and verify window closure
input.pipeInput("key-1", event1, Instant.ofEpochSecond(100));
input.pipeInput("key-1", event2, Instant.ofEpochSecond(200));
driver.advanceWallClockTime(Duration.ofSeconds(300));
assertThat(output.readValueToMap()).containsEntry("key-1", 2L);
// Verify state store after processing
KeyValueStore<String, Long> store = driver.getKeyValueStore("customer-totals");
assertThat(store.get("cust-42")).isEqualTo(3L);
// Verify TTL cleanup
input.pipeInput("key-1", event1);
driver.advanceWallClockTime(Duration.ofHours(25));
driver.runScheduledPunctuators();
assertThat(store.get("key-1")).isNull();
# In CI pipeline before deploy
- name: Validate schema compatibility
run: |
curl -X POST $SCHEMA_REGISTRY_URL/compatibility/subjects/orders-value/versions/latest \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d @new-schema.json
Consumer-driven contracts. Consumer describes expectations, producer verifies in CI.
For the Java ecosystem. Groovy DSL for describing contracts, auto-generated tests.
@Container
KafkaContainer<?> kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));
@Test
void should_rebalance_and_resume() {
// 1. Start consumer 1
// 2. Process some events
// 3. Kill consumer 1
// 4. Start consumer 2 — it should pick up partitions
// 5. Verify all events processed
}
| Platform | Local Emulator | Limitations |
|---|---|---|
| AWS Kinesis | LocalStack | Not all features, different latency |
| Google Pub/Sub | Official emulator | Limited ordering support |
| Azure Event Hubs | No full emulator | Testing against dev environments |
Using a single shared cluster for all teams turns it into a "dumpster". Test topics with suffixes -test-v2-final overlap with other tests, data becomes stale, and load from one team's tests breaks another's. This is the "tragedy of the commons" to avoid.
Ephemeral clusters as the solution: test ran → cluster spun up → filled with synthetics → tests passed → cluster destroyed. Expensive but the only way to guarantee isolation.
Kills random pods in Kubernetes. Verifies the service recovers.
Chaos experiments for Kubernetes. Ready scenarios: pod-delete, network-loss, disk-fill.
Emulates network failures: delay, break, packet loss. Verifies retry works.
Managed chaos. Safe scenarios. For orgs without their own chaos platform.
| Tool | Language | Feature |
|---|---|---|
| k6 | JavaScript | Modern, built-in metrics, cloud version |
| Gatling | Scala DSL | Powerful DSL, out-of-the-box reports |
| JMeter | Java + UI | Classic, many plugins, UI |
| Locust | Python | Simple DSL, distributed load |
// Temporal: testing compensation
@Test
void should_compensate_when_payment_fails() {
// 1. Start workflow with mock activities
// 2. Simulate payment step failure
// 3. Verify compensations called in reverse order
// 4. Verify final state is consistent
}
| Level | Cost | When Justified |
|---|---|---|
| Unit + Property | ~$0 | Always |
| Testcontainers | CPU/RAM locally or in CI | Every PR |
| Schema testing | ~$0 | Every PR |
| Contract testing | ~$0 | Every PR |
| Ephemeral clusters | $$–$$$ | Before release |
| Chaos testing | $$ | Once a month |
| Load testing | $$–$$$ | Before major release |
| Production testing | Infrastructure + risk | Critical services |
If writing a test takes longer than writing code — the test won't be written. Invest in testing infrastructure the same way you invest in production infrastructure.
| Chapter | Focus | Link to This Chapter |
|---|---|---|
| III. The Anatomy of an Ideal Event | Schema Registry, evolution | Schema testing, snapshot testing |
| V. The Mechanics of Stream Processing | TopologyTestDriver, state stores | Level 1 testing |
| VI. Time in Streams | Watermarks, event time | Testing watermarks, late events |
| VII. The Memory of Microservices | State store testing | Testing stateful processors |
| VIII. The Mechanics of Business Processes | Compensations, human-in-the-loop | Testing Saga and compensations |
| X. Basic Producer/Consumer Pattern | Simple poll-process-produce | Base level of testing |
| XI. Anatomy of Heavyweight Frameworks | MiniCluster, TestHarness | Testing Flink jobs |
| XII. Anatomy of Lightweight Frameworks | TopologyTestDriver, EmbeddedKafka | Testing Kafka Streams |
| XIII. A Bridge Between Worlds | Contract testing | Testing between API and events |
| XIV. The Microservices Tax | Testing infrastructure | Testing as part of the platform |
| XV. How to Test the Unobservable | Testing EDA | QA perspective of the entire series |
There is no single "right" way to test event-driven microservices. The modularity of microservices gives you the freedom to choose the tool for the task. Invest in automation of test environment creation and data generation, and your QA process will stop being a "bottleneck", becoming a reliable foundation for Continuous Delivery.
Chapter XV of the "Event-Driven Architecture" series. Sources: Apache Kafka, Confluent, Apache Flink, Testcontainers, Pact, Temporal, LitmusChaos, k6, jqwik, Hypothesis, production system case studies, FinCodeCow experience.