Kafka для QA Automation на Java: как тестировать события и не писать flaky-автотесты
Практический гайд по Apache Kafka для Java QA Automation: что должен знать тестировщик, как проверять события, consumer-тесты, offsets, idempotency, Awaitility, Testcontainers, типичные ошибки и best practices для стабильных автотестов.
Коротко: зачем QA Automation инженеру Kafka
Kafka нужна QA Automation инженеру, когда продукт живёт не только в HTTP-запросах и UI-кнопках, а в событиях: заказ создан, платёж подтверждён, курьер назначен, письмо отправлено, бонусы начислены, статус изменён. В таких системах баг может быть не в API-ответе, а в том, что событие не ушло, ушло два раза, ушло не с тем ключом, пришло не в тот topic или обработалось неидемпотентно.
Хороший Java QA Automation Engineer не обязан быть Kafka-админом, но должен понимать topic, partition, key, producer, consumer, consumer group, offset, retry, idempotency и eventual consistency. Без этого автотесты на event-driven системе превращаются в случайные sleep, нестабильные проверки и фразы «у меня локально проходит».
Что такое Kafka простыми словами
Apache Kafka — это распределённая платформа для хранения и передачи событий. Сервис-продюсер публикует событие в topic, Kafka сохраняет его в лог, а сервисы-консьюмеры читают события и обрабатывают их в своём темпе. В отличие от обычного HTTP-вызова, отправитель и получатель не обязаны работать синхронно в один момент.
- ▸Topic — логическая лента событий, например order.created или payment.status.changed.
- ▸Message / record — конкретное событие внутри topic.
- ▸Key — ключ события; влияет на partition и порядок обработки для одного бизнес-объекта.
- ▸Partition — часть topic, внутри которой Kafka сохраняет порядок сообщений.
- ▸Producer — сервис, который отправляет события.
- ▸Consumer — сервис или тест, который читает события.
- ▸Consumer group — группа consumer-ов, которые совместно читают topic.
- ▸Offset — позиция consumer-а в partition: до какого сообщения он дочитал.
Главное правило для тестировщика: Kafka не гарантирует глобальный порядок всех сообщений в topic. Порядок есть внутри partition. Поэтому если порядок важен для заказа, платежа или пользователя, тест должен проверять key и понимать, почему события одного объекта должны попадать в одну partition.
Какие Kafka-сценарии должен уметь тестировать QA
- ▸Сервис публикует событие после действия: заказ создан, статус изменён, оплата прошла.
- ▸Событие содержит корректный payload: id, status, amount, currency, timestamps, nested objects.
- ▸Событие уходит в правильный topic и с правильным key.
- ▸Consumer корректно обрабатывает входящее событие и меняет состояние системы.
- ▸Повторная доставка не ломает данные: idempotency работает.
- ▸Некорректное событие не роняет consumer навсегда и уходит в retry / dead-letter flow.
- ▸События по одному бизнес-объекту не обрабатываются в неправильном порядке.
- ▸Система становится консистентной через некоторое время, а не мгновенно после HTTP-ответа.
Это уже уровень выше базового REST Assured и Selenide. Поэтому Kafka хорошо отличает сильного Java QA Automation кандидата от человека, который умеет только нажать кнопку и проверить текст на странице.
Best practice 1. Проверяйте бизнес-событие, а не только факт сообщения
Плохой Kafka-тест проверяет: «в topic что-то пришло». Хороший Kafka-тест проверяет: «после создания заказа пришло событие order.created с id этого заказа, корректным userId, суммой, статусом CREATED, версией схемы и correlationId».
1OrderResponse order = orderApi.createOrder(defaultOrderRequest());
2
3OrderCreatedEvent event = kafkaBus.awaitEvent(
4 "order.created",
5 OrderCreatedEvent.class,
6 e -> e.orderId().equals(order.id())
7);
8
9assertThat(event.status()).isEqualTo("CREATED");
10assertThat(event.userId()).isEqualTo(order.userId());
11assertThat(event.totalAmount()).isEqualByComparingTo(order.totalAmount());
12assertThat(event.correlationId()).isNotBlank();Если тест не связывает событие с конкретным действием, он легко поймает старое сообщение от другого теста и даст ложный зелёный результат.
Best practice 2. Никогда не используйте Thread.sleep для Kafka
Kafka-событие асинхронно: оно может прийти через 50 мс, через 2 секунды или не прийти вообще. Thread.sleep(5000) делает тест медленным и всё равно не гарантирует стабильность. Правильный подход — ждать условие до timeout: событие найдено, запись появилась в БД, статус изменился.
1await()
2 .atMost(Duration.ofSeconds(10))
3 .pollInterval(Duration.ofMillis(200))
4 .untilAsserted(() -> {
5 var events = kafkaBus.readEvents("order.created", OrderCreatedEvent.class);
6
7 assertThat(events)
8 .anyMatch(event -> event.orderId().equals(orderId));
9 });Awaitility или похожий механизм делает тест детерминированным: тест ждёт ровно столько, сколько нужно, и падает с понятной ошибкой, если условие не выполнилось.
Best practice 3. Генерируйте уникальный consumer group для теста
Consumer group хранит offset. Если несколько тестов используют одну group.id, они могут мешать друг другу: один тест прочитал сообщение, offset сдвинулся, второй тест уже его не увидел. Для автотестов безопаснее создавать уникальный group.id на тест или на test class.
1String groupId = "qa-test-" + UUID.randomUUID();
2
3Properties props = new Properties();
4props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
5props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
6props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
7props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");Для тестов, которые проверяют новые события после действия, часто используют уникальную group и фильтр по бизнес-id. Так тест не зависит от offset-истории предыдущих прогонов.
Best practice 4. Фильтруйте события по business id
Самая частая ошибка Kafka-тестов — взять первое сообщение из topic и считать его нужным. На живом стенде topic может содержать события от других тестов, ручных проверок, фоновых задач и соседних команд. Поэтому у каждого теста должен быть уникальный business id: orderId, userId, paymentId, requestId или correlationId.
- ▸Создали заказ — ждите событие именно с этим orderId.
- ▸Создали пользователя — ждите событие именно с этим userId.
- ▸Запустили платёж — ждите событие именно с этим paymentId.
- ▸Если бизнес-id появляется не сразу, прокидывайте correlationId из HTTP-запроса в событие.
Фильтр по business id важнее, чем длинный timeout. Без фильтра тест может быть зелёным, но проверять чужой сценарий.
Best practice 5. Проверяйте key, partition и ordering там, где это важно
Если система должна обрабатывать события заказа строго по порядку, key должен быть связан с orderId. Тогда события одного заказа попадут в одну partition и сохранят порядок внутри неё. Если key случайный, порядок обработки может стать недетерминированным.
1ConsumerRecord<String, OrderEvent> record = kafkaBus.awaitRecord(
2 "order.events",
3 r -> r.value().orderId().equals(orderId)
4);
5
6assertThat(record.key()).isEqualTo(orderId);
7assertThat(record.value().version()).isEqualTo(2);Не каждый тест обязан проверять partition. Но если баги в системе связаны с out-of-order обработкой, key и порядок событий становятся частью тестового оракула.
Best practice 6. Проверяйте idempotency повторной доставкой события
Kafka и распределённые системы живут в мире at-least-once доставки: одно и то же событие может быть обработано повторно. Поэтому consumer должен быть идемпотентным: повторная обработка не должна создавать дубль заказа, двойное списание, второй email или лишнюю запись в истории.
1PaymentCapturedEvent event = new PaymentCapturedEvent(paymentId, orderId, amount);
2
3kafkaProducer.send("payment.captured", paymentId, event);
4kafkaProducer.send("payment.captured", paymentId, event);
5
6await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> {
7 Order order = orderRepository.findById(orderId);
8
9 assertThat(order.status()).isEqualTo(PAID);
10 assertThat(paymentRepository.countCaptures(paymentId)).isEqualTo(1);
11});Это один из самых полезных классов Kafka-тестов, потому что реальные баги часто возникают не на happy path, а на retry, повторной доставке и частичных сбоях.
Best practice 7. Не тестируйте Kafka через UI
Если кнопка в UI создаёт заказ, а заказ публикует событие, не нужно весь Kafka-тест строить через браузер. UI добавит медленность, flaky-локаторы и лишние причины падения. Проверяйте Kafka на уровне API, БД и consumer-ов. UI оставьте для пары критичных end-to-end сценариев.
| Что проверяем | Лучший уровень проверки | Почему |
|---|---|---|
| Событие опубликовано | API + Kafka consumer | быстрее и точнее UI |
| Consumer изменил состояние | Kafka producer + DB/API assertion | изолируем обработчик |
| Пользователь видит новый статус | UI smoke | проверяем интеграцию целиком |
| Повторная доставка | Kafka producer + DB assertion | UI здесь только мешает |
Best practice 8. Поднимайте Kafka в тестах через Testcontainers
Для локальных и интеграционных тестов удобно поднимать Kafka через Testcontainers. Это снижает зависимость от внешнего стенда: тест сам стартует broker, получает bootstrapServers и работает с изолированной инфраструктурой.
1static KafkaContainer kafka = new KafkaContainer(
2 DockerImageName.parse("apache/kafka-native:3.8.0")
3);
4
5@BeforeAll
6static void startKafka() {
7 kafka.start();
8 bootstrapServers = kafka.getBootstrapServers();
9}Testcontainers особенно полезен для проверки ваших Kafka utilities, producer/consumer wrappers, сериализации, схем и локальных интеграционных тестов. Для full e2e на staging всё равно нужен стенд, похожий на production.
Best practice 9. Проверяйте schema compatibility и обязательные поля
Kafka-событие — это контракт между сервисами. Если producer удалил поле, переименовал enum или поменял тип amount со string на number, consumer может сломаться без изменения HTTP API. Поэтому тесты должны проверять не только бизнес-значения, но и контракт события.
- ▸Обязательные поля присутствуют и не null.
- ▸Enum-значения соответствуют договорённости.
- ▸Дата и время передаются в ожидаемом формате.
- ▸Версия схемы или eventType указаны явно.
- ▸Backward compatibility проверяется отдельно, если есть Schema Registry.
- ▸Unknown fields не должны ломать consumer, если контракт это допускает.
Если в проекте нет Schema Registry, QA всё равно может ловить контрактные поломки через DTO, JSON Schema, snapshot-проверки или consumer-driven tests.
Best practice 10. Тестируйте негативные события и dead-letter flow
Happy path в Kafka обычно проверяют все. А вот некорректный payload, неизвестный status, отсутствующий id, невалидный amount и повторная доставка часто остаются без покрытия. Именно там ломаются consumer-ы.
- ▸Событие без обязательного поля не должно бесконечно перезапускать consumer.
- ▸Невалидный payload должен попадать в error topic или dead-letter topic, если такая архитектура предусмотрена.
- ▸Ошибка внешнего сервиса должна приводить к retry, а не к потере события.
- ▸После исчерпания retry событие должно быть диагностируемым: key, payload, error reason, timestamp.
Для QA это важный навык: вы проверяете не только «работает ли система», но и «понятно ли, что произошло, когда система не смогла обработать событие».
Мини-фреймворк KafkaTestBus для автотестов
В реальном проекте не стоит писать KafkaConsumer руками в каждом тесте. Лучше сделать небольшой слой KafkaTestBus: он умеет читать topic, десериализовать события, фильтровать по predicate, ждать событие до timeout и возвращать typed DTO.
1public class KafkaTestBus {
2 public <T> T awaitEvent(
3 String topic,
4 Class<T> eventType,
5 Predicate<T> filter
6 ) {
7 AtomicReference<T> result = new AtomicReference<>();
8
9 await().atMost(Duration.ofSeconds(10)).until(() -> {
10 List<T> events = readEvents(topic, eventType);
11 events.stream().filter(filter).findFirst().ifPresent(result::set);
12 return result.get() != null;
13 });
14
15 return result.get();
16 }
17}Такой слой делает тесты читаемыми. В тесте остаётся бизнес-смысл: создать заказ, дождаться order.created, проверить payload. Детали consumer-а, polling и JSON-десериализации уходят в инфраструктуру.
Чеклист хорошего Kafka-теста
- ▸Тест создаёт уникальные данные и фильтрует события по business id.
- ▸Consumer group уникальна или offset контролируется явно.
- ▸Нет Thread.sleep; ожидания построены через condition + timeout.
- ▸Проверяется payload, key и topic, а не просто факт наличия сообщения.
- ▸Тест не зависит от порядка чужих сообщений в topic.
- ▸Ассерты объясняют бизнес-ожидание, а не внутренности Kafka.
- ▸Негативные и retry-сценарии покрыты отдельно.
- ▸Данные чистятся или изолируются так, чтобы параллельный запуск не ломал тесты.
- ▸Отчёт содержит enough debug info: topic, key, correlationId, payload fragment.
- ▸Kafka utility переиспользуется, а не копипастится в каждом тесте.
Типичные ошибки при тестировании Kafka
| Ошибка | Почему плохо | Как правильно |
|---|---|---|
| Thread.sleep | медленно и нестабильно | Awaitility / polling до условия |
| читать первое сообщение | можно поймать чужое событие | фильтр по business id |
| общий group.id | тесты двигают offset друг другу | уникальная group на тест |
| тестировать всё через UI | медленно и flaky | API + Kafka + DB |
| не проверять key | можно потерять порядок | проверять key для ordered flow |
| не проверять retry | баги всплывают в production | негативные события + DLT |
| копипастить consumer | сложно поддерживать | KafkaTestBus / test utilities |
Что спрашивают на собеседовании по Kafka у Java QA
- ▸Что такое topic, partition, offset и consumer group?
- ▸Почему порядок сообщений гарантируется только внутри partition?
- ▸Зачем нужен message key?
- ▸Чем at-most-once отличается от at-least-once?
- ▸Почему consumer может обработать сообщение два раза?
- ▸Что такое idempotency и как её тестировать?
- ▸Почему Thread.sleep в async-тестах — плохая практика?
- ▸Как проверить, что сервис отправил событие после API-запроса?
- ▸Как тестировать consumer, который читает Kafka и пишет в БД?
- ▸Что делать с невалидным событием: retry, DLT, error topic?
Как это связано с курсом Java QA Automation 2026
В обновлённом курсе Java QA Automation 2026 Kafka разбирается не как абстрактная технология из презентации, а как часть тестового фреймворка вокруг реального backend-приложения ShawarmaShop. Студент учится публиковать события, читать их consumer-ом, ждать бизнес-событие без sleep, проверять payload, строить KafkaTestBus и тестировать async-сценарии вместе с API, БД, Docker и CI/CD.
Это важное отличие от базовых Selenium-курсов: современные Java QA Automation вакансии всё чаще требуют понимать backend и event-driven архитектуру. Kafka — один из навыков, который помогает выйти за рамки Junior UI-автотестов.