惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

AI
AI
博客园 - 叶小钗
Blog — PlanetScale
Blog — PlanetScale
Microsoft Azure Blog
Microsoft Azure Blog
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
MyScale Blog
MyScale Blog
大猫的无限游戏
大猫的无限游戏
A
About on SuperTechFans
量子位
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
Martin Fowler
Martin Fowler
阮一峰的网络日志
阮一峰的网络日志
D
Docker
Jina AI
Jina AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Register - Security
The Register - Security
J
Java Code Geeks
S
SegmentFault 最新的问题
月光博客
月光博客
G
Google Developers Blog
美团技术团队
Last Week in AI
Last Week in AI
L
LangChain Blog
Apple Machine Learning Research
Apple Machine Learning Research
T
The Blog of Author Tim Ferriss
腾讯CDC
Recent Announcements
Recent Announcements
Recorded Future
Recorded Future
The Cloudflare Blog
有赞技术团队
有赞技术团队
博客园_首页
博客园 - 聂微东
人人都是产品经理
人人都是产品经理
B
Blog
I
InfoQ
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
F
Fortinet All Blogs
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
MongoDB | Blog
MongoDB | Blog
爱范儿
爱范儿
D
DataBreaches.Net
F
Full Disclosure
M
MIT News - Artificial intelligence
博客园 - 司徒正美
H
Help Net Security

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Kafka Consumer в тестовой автоматизации: архитектурный разбор
Egor1301 · 2026-06-25 · via Все публикации подряд на Хабре

Средний

16 мин

222

Привет, Хабр! На связи Егор Лаптев — QA Fullstack Java в SENSE на проекте крупного российского банка.

End-to-end тесты UI проверяют только то, что видно на экране: кнопка нажалась, форма открылась, данные отобразились. Но в распределённых системах значительная часть бизнес-логики уходит за кадр — в асинхронные события, которые летят через Kafka. Если событие не дошло до топика или пришло с неверным payload, пользователь этого не увидит, а бизнес-процесс сломается.

В этой статье расскажу, как мы научились проверять Kafka-события прямо в автотестах — без Kafka UI, kcat и обёрточных сервисов, одной зависимостью и так, чтобы это работало в корпоративной сети с SSL. Покажу архитектуру коннектора, разберу три основных проблемы (SSL-сертификаты, конфликты consumer group и асинхронные тайминги), и поделюсь моделью работы инженера в связке с AI-агентом.

Кому будет полезно: QA-инженерам и специалистам по автоматизации, которые тестируют распределённые системы и хотят проверять Kafka-события прямо в автотестах; и инженерам, которым интересна практическая модель работы в связке с AI-агентом — как она ускоряет разработку тестовой инфраструктуры.

Контекст проекта

Мы столкнулись с тем, что тесты показывали зелёный, а в продакшене сыпались инциденты, потому что никто не проверял, доходят ли события до Kafka. Готового решения, которое можно было бы просто встроить в автотесты, не нашлось: всё либо тянет за собой инфраструктуру, либо не работает в корпоративных сетях с SSL.

Задача была в том, чтобы снизить зависимость от внешних источников и ручных проверок. Раньше верификация Kafka-событий была ручной: тестировщик запускал тест, шёл в Kafka UI или консоль, искал сообщение, проверял payload. Это медленно, ненадёжно и не масштабируется. Мы хотели, чтобы проверка событий была такой же автоматизированной, как и проверка UI.

При этом сам коннектор — лишь часть более широкого подхода. Он не появился бы сам по себе: его сделал возможным принцип, при котором инженер по автоматизации не ограничивается ролью «пишу тесты по ТЗ», а ищет точки входа — места, где ручной труд можно заменить автоматизацией, а автоматизацию усилить AI. AI-агент при этом встроен в процессы как полноценный участник команды: от исследования и генерации кода до отладки и ревью. Коннектор — один из результатов такого подхода.

Что получилось: полностью самостоятельный коннектор без обёрточных сервисов. Никакого Kafka UI, Confluent Control Center, Kafka REST Proxy или kcat — только прямое подключение через kafka-clients. Одна зависимость в build.gradle, никаких контейнеров и прокси. Работает локально, в CI/CD, в Docker — достаточно указать bootstrapServers и креды.

Что мы получили в цифрах:

Метрика

До

После

Δ

Время верификации события

~3 мин вручную

~5 сек автоматически

в 36 раз быстрее

Процент пропущенных багов в Kafka-событиях

~40% от всех прод-инцидентов

<5%

в 8 раз меньше

Покрытие бизнес-сценариев с Kafka-проверками

0 сценариев

15+ сценариев

с нуля

Время разработки нового Kafka-теста

~2 дня вручную

~2 часа с готовым коннектором

в 8 раз быстрее

Какие боли решает коннектор

#

Боль

Решение

1

SSL-сертификаты — корпоративный CA не входит в Java truststore, consumer падает с SSLHandshakeException

Кастомная SslEngineFactory — уникальная реализация, которая инжектируется напрямую в consumer и доверяет любому сертификату

2

Consumer Group конфликты — автотесты с тем же groupId отбирают партиции у реальных сервисов, тестовая среда ломается

Уникальный groupId для автотестов + режим assignAllPartitions() без участия в consumer group

3

Асинхронные тайминги — Thread.sleep() то недожидается, то ждёт впустую, тесты флакают

Poll-цикл с настраиваемым таймаутом, сообщение захватывается в момент появления

4

Ручная верификация — тестировщик идёт в Kafka UI, ищет сообщение, проверяет payload руками

BDD-шаги: подключение, чтение, фильтрация, проверка — всё в сценарии

5

Внешние зависимости — Kafka REST Proxy, kcat, контейнеры — требуют поддержки и не работают везде

Прямое подключение через kafka-clients, одна зависимость, работает везде

Дальше — детальный разбор каждой боли и решения.

Архитектура

src/test/java/com/example/
├── client/kafka/
│   ├── KafkaConsumerClientjava 	— ядро: consumer, poll, фильтрация
 |── CustomSslEngineFactory.java   — SSL-обход для корпоративных сертификатов
├── steps/
│   └── KafkaBddSteps.java        	— BDD-шаги (Given/When/Then)
└── utils/
	└── ScenarioContext.java       	— Singleton-хранилище состояния

 
src/test/resources/
├── configuration/config.properties	— Kafka-конфигурация (camelCase)
└── .env                           	— Kafka-конфигурация (UPPER_SNAKE_CASE)

Зависимость (build.gradle):

testImplementation 'org.apache.kafka:kafka-clients:3.7.0'

Consumer-обёртка — ядро коннектора

Два конструктора

Конструктор

Назначение

По умолчанию

Читает конфигурацию из config.properties через конфигурационный ридер

С явными параметрами (bootstrapServers, saslMechanism, saslUsername, saslPassword, groupId, securityProtocol)

Используется для кастомного groupId

Три режима подключения

Метод

Режим

Offset

Когда использовать

subscribe(topic)

Consumer Group

Отслеживается группой

Чтение новых сообщений после подписки

assignPartition(topic, partition)

Прямое назначение

seekToBeginning

Чтение конкретной партиции с начала

assignAllPartitions(topic)

Все партиции

seekToBeginning

Полное чтение топика с начала

Ключевая разница: subscribe() участвует в consumer group — offset фиксируется. assign*() — прямое назначение партиций, без влияния на consumer group offset.

Чтение сообщений

Метод

Возвращает

Особенность

pollMessages(timeoutSeconds)

List<ConsumerRecord<String, String>>

Цикл poll по 1 сек до таймаута

pollOneMessage(timeoutSeconds)

ConsumerRecord<String, String>

Возвращает первое же сообщение

pollMessageValues(timeoutSeconds)

List<String>

Только значения (без ключей/метаданных)

Все прочитанные сообщения накапливаются во внутреннем буфере — это позволяет фильтровать их позже.

Фильтрация накопленных сообщений

findMessagesContaining(substring)
findMessagesByKey(key)
getConsumedMessages()
clearConsumedMessages()

Свойства Consumer

Consumer настраивается со следующими ключевыми свойствами: чтение с начала топика (earliest), ручной коммит offset после обработки и лимит в 100 записей за один poll. Десериализаторы ключей и значений — строковые.

auto.offset.reset=earliest
enable.auto.commit=false
max.poll.records=100
key.deserializer=StringDeserializer
value.deserializer=StringDeserializer

Жизненный цикл

Класс реализует AutoCloseable — закрытие consumer'а гарантировано в хуке после сценария:

@After
public void closeKafkaConsumer() {
	Object consumer = context.getData("consumerKey");
	if (consumer instanceof KafkaConsumerWrapper) {
    	((KafkaConsumerWrapper) consumer).close();
	}
}

CustomSslEngineFactory — метод configure()

public class CustomSslEngineFactory implements SslEngineFactory {

    private SSLContext sslContext;

    @Override
    public void configure(Map<String, ?> configs) {
        try {
            TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }
                    public void checkClientTrusted(X509Certificate[] certs,
                                                    String authType) {}
                    public void checkServerTrusted(X509Certificate[] certs,
                                                    String authType) {}
                }
            };
            sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustAllCerts, new SecureRandom());
        } catch (Exception e) {
            throw new RuntimeException(
                    "Не удалось инициализировать CustomSslEngineFactory", e);
        }
    }

    @Override
    public SSLEngine createClientSslEngine(String peerHost, int peerPort,
                                            String endpointIdentification) {
        SSLEngine engine = sslContext.createSSLEngine(peerHost, peerPort);
        engine.setUseClientMode(true);
        engine.setSSLParameters(new SSLParameters());
        return engine;
    }
// shouldBeRebuilt, reconfigurableConfigs, keystore, truststore, close — бойлерплейт
}

KafkaConsumerClient — подключение и poll-цикл

public class KafkaConsumerClient implements AutoCloseable {
    private KafkaConsumer<String, String> consumer;
    private final List<ConsumerRecord<String, String>> consumedMessages = new ArrayList<>();
    // ... конструкторы, поля, buildConsumerProperties() ...
    @Step("Подписка на Kafka топик: {topic}")
    public void subscribe(String topic) {
        consumer = new KafkaConsumer<>(buildConsumerProperties());
        consumer.subscribe(Collections.singletonList(topic));
        consumer.poll(Duration.ofMillis(3000));
    }
    @Step("Назначение партиции топика: {topic}, партиция: {partition}")
    public void assignPartition(String topic, int partition) {
        consumer = new KafkaConsumer<>(buildConsumerProperties());
        TopicPartition tp = new TopicPartition(topic, partition);
        consumer.assign(Collections.singletonList(tp));
        consumer.seekToBeginning(Collections.singletonList(tp));
    }
    @Step("Назначение всех партиций топика: {topic}")
    public void assignAllPartitions(String topic) {
        consumer = new KafkaConsumer<>(buildConsumerProperties());
        List<TopicPartition> partitions = consumer.partitionsFor(topic).stream()
                .map(pi -> new TopicPartition(topic, pi.partition()))
                .toList();
        consumer.assign(partitions);
        consumer.seekToBeginning(partitions);
    }
    @Step("Чтение сообщений из Kafka топика: {timeoutSeconds} сек")
    public List<ConsumerRecord<String, String>> pollMessages(int timeoutSeconds) {
        List<ConsumerRecord<String, String>> result = new ArrayList<>();
        long endTime = System.currentTimeMillis() + timeoutSeconds * 1000L;
        while (System.currentTimeMillis() < endTime) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> record : records) {
                result.add(record);
                consumedMessages.add(record);
            }
            if (!records.isEmpty()) {
                consumer.commitSync();
            }
        }
        return result;
    }
    @Step("Чтение одного сообщения из Kafka топика")
    public ConsumerRecord<String, String> pollOneMessage(int timeoutSeconds) {
        long endTime = System.currentTimeMillis() + timeoutSeconds * 1000L;
        while (System.currentTimeMillis() < endTime) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
            if (!records.isEmpty()) {
                ConsumerRecord<String, String> record = records.iterator().next();
                consumedMessages.add(record);
                consumer.commitSync();
                return record;
            }
        }
        return null;
    }
    // findMessagesContaining, findMessagesByKey, getConsumedMessages,
    // clearConsumedMessages, close — опущены для краткости
}

KafkaSteps — примеры BDD-шагов

public class KafkaSteps {
    @Дано("подключение к Kafka топику {string}")
    public void subscribeToTopic(String topic) {
        getConsumer().subscribe(topic);
    }
    @Дано("назначение всех партиций Kafka топика {string}")
    public void assignAllPartitions(String topic) {
        getConsumer().assignAllPartitions(topic);
    }
  
    @Когда("прочитаны сообщения из Kafka топика в течение {int} секунд")
    public void pollMessages(int timeoutSeconds) {
        List<ConsumerRecord<String, String>> messages =
                getConsumer().pollMessages(timeoutSeconds);
        ScenarioContext.getInstance().setData("kafkaMessages", messages);
    }

    @Тогда("в Kafka топике есть сообщения")
    public void verifyMessagesExist() {
        @SuppressWarnings("unchecked")
        List<ConsumerRecord<String, String>> messages = (List<ConsumerRecord<String, String>>)
                ScenarioContext.getInstance().getData("kafkaMessages");
        assertNotNull(messages);
        assertFalse(messages.isEmpty(), "В Kafka топике нет сообщений");
    }
  
    @Тогда("сообщение из Kafka топика содержит текст {string}")
    public void verifyMessageContainsText(String expectedText) {
        @SuppressWarnings("unchecked")
        List<ConsumerRecord<String, String>> messages = (List<ConsumerRecord<String, String>>)
                ScenarioContext.getInstance().getData("kafkaMessages");
        boolean found = messages.stream()
                .anyMatch(r -> r.value() != null && r.value().contains(expectedText));
        assertTrue(found, "Ни одно сообщение не содержит текст: " + expectedText);
    }
  
    @Тогда("значение последнего сообщения из Kafka топика сохранено в контекст как {string}")
    public void saveLastMessageValueToContext(String key) {
        ConsumerRecord<String, String> message = (ConsumerRecord<String, String>)
                ScenarioContext.getInstance().getData("kafkaLastMessage");
        assertNotNull(message, "Сообщение не было прочитано");
        ScenarioContext.getInstance().setData(key, message.value());
    }
}

1. SSL-сертификаты: когда Java отказывается подключаться

Kafka-кластер в корпоративной среде работает через SASL_SSL с внутренними сертификатами, которые не входят в стандартный Java truststore. При первой попытке подключения consumer падал с SSLHandshakeException — Java просто не доверяла корпоративному CA.

Что пробовали:

  • Импорт сертификата в JVM truststore (keytool -import) — работает на одной машине, но ломается в CI/CD, где агенты динамические и truststore нужно настраивать на каждом раннере;

  • Передача truststore через ssl.truststore.location — требует хранения JKS-файла и его синхронизации между средами;

  • Обёрточные сервисы (Kafka REST Proxy, kcat) — тянут за собой инфраструктуру, контейнеры, поддержку.

Что сделали: написали собственную CustomSslEngineFactory — полностью уникальную реализацию интерфейса SslEngineFactory из kafka-clients. Это не обёртка над готовой библиотекой и не костыль с TrustManager[] на уровне HttpsURLConnection. Это кастомная фабрика SSL-движков, которая инжектируется напрямую в Kafka consumer через конфигурацию:

props.put("ssl.engine.factory.class", "com.example.client.kafka.CustomSslEngineFactory");

Фабрика создаёт SSLContext с TrustManager, который принимает любой сертификат, и оборачивает его в SSLEngine через createSSLEngine(String peerHost, int peerPort). Без этого коннектор физически не мог бы работать в корпоративной сети — ни один стандартный подход не подходил для динамических CI/CD-агентов.

⚠️ Внимание: данный подход небезопасен для продакшена. TrustAllSslEngineFactory отключает проверку SSL-сертификатов, что создаёт уязвимость к MITM-атакам. Мы применяем это решение исключительно внутри изолированного тестового контура (Staging/QA), где:

  • Kafka-кластер недоступен извне корпоративной сети;

  • все подключённые сервисы — тестовые стенды, не обрабатывающие реальные данные клиентов;

  • доступ к кластеру ограничен SASL/SCRAM аутентификацией.

В продакшене необходимо использовать стандартную проверку сертификатов через JKS truststore. Если ваша тестовая среда позволяет импортировать корпоративный CA в JVM truststore — это предпочтительный вариант. Наш подход — компромисс для динамических CI/CD-агентов, где truststore невозможно настроить статически.

Здесь AI-агент проявил себя особенно ярко: реализация SslEngineFactory — это не стандартный паттерн, который можно нагуглить. Потребовалось понимание внутренних механизмов kafka-clients и Java SSL API. AI-агент сгенерировал корректную реализацию с первого раза, включая все методы интерфейса — createSSLEngine, createClientSslEngine, createServerSslEngine, shouldBeRebuilt и другие. Это тот случай, когда AI не просто «помог с кодом», а решил задачу, которая без него заняла бы дни изучения документации.

2. Consumer Group конфликты: когда автотесты ломают тестовую среду

Первая версия коннектора использовала subscribe() с дефолтным groupId из конфигурации. На первый взгляд — всё работало. Но при параллельном запуске тестов обнаружилось, что реальные сервисы-консьюмеры на тестовой среде перестали получать сообщения. Причина: автотесты с тем же groupId участвовали в rebalance и «отбирали» партиции у рабочих сервисов.

Что сделали:

  1. Ввели правило: автотесты всегда используют уникальный groupId, не пересекающийся с группами реальных сервисов;

  2. Добавили второй режим подключения — assignAllPartitions() — для чтения с начала топика без участия в consumer group вообще. Никакого rebalance, никаких конфликтов;

  3. Добавили BDD-шаг с кастомным groupId для параллельных прогонов — каждый сценарий получает свою группу.

Четыре правила работы с consumer groups в автотестах:

Правило №1: всегда выделяйте отдельную consumer group для автотестов. В конфигурации автотестов должен быть уникальный groupId, который не пересекается с группами реальных сервисов:

kafkaGroupId=some-topic.ui-autotests

Правило №2: если нужно читать с начала топика — используйте assignAllPartitions(). Режим assign не участвует в consumer group rebalance и не влияет на другие консьюмеры.

Правило №3: подключайтесь к Kafka ДО триггерного действия. Consumer должен успеть подписаться и пройти rebalance до того, как тестируемое действие отправит сообщение. Иначе сообщение может быть прочитано другим участником группы или пропущено:

1. [шаг подключения к топику...]

2. [триггерное действие, порождающее событие...]

3. [шаг чтения сообщений с таймаутом...]

Правило №4: для параллельных тестов — уникальный groupId на каждый сценарий. Если тесты запускаются параллельно и используют один groupId, они будут конкурировать за партиции внутри одной consumer group. Решение — генерировать уникальный groupId для каждого сценария.

Эта проблема — классический пример того, как важно не просто написать код, а понять контекст: как система работает в целом, кто ещё читает из топика, что произойдёт при параллельном запуске. Инженер, который ищет точки входа для улучшения процессов, видит такие риски до того, как они превращаются в инциденты.

3. Асинхронные тайминги: когда sleep() не работает

Сообщение в Kafka появляется не мгновенно после действия в системе — между триггером и появлением события в топике проходит от нескольких секунд до минуты. Первая попытка использовать Thread.sleep(10000) привела к двум проблемам: иногда 10 секунд не хватало (тест падал), иногда сообщение появлялось за 2 секунды (тест ждал впустую 8 секунд, замедляя прогон).

Что сделали: реализовали poll-цикл с настраиваемым таймаутом. Consumer опрашивает топик каждую секунду (poll(Duration.ofSeconds(1))) до тех пор, пока не получит сообщение или не истечёт таймаут. Сообщение захватывается в момент появления — без лишних ожиданий и без пропусков:

while (elapsed < timeoutSeconds) {
	ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
	if (!records.isEmpty()) {
    	break;
	}
	elapsed++;
}

4. Ручная верификация → BDD-шаги

Раньше тестировщик после запуска теста шёл в Kafka UI или консоль, искал сообщение, проверял payload руками. Это медленно, ненадёжно и не масштабируется. Коннектор заменяет весь этот процесс на BDD-шаги, которые выполняются прямо в сценарии.

Подключение (Given / Дано)

Категория шага

Описание

Подписка на топик

Через consumer group с groupId по умолчанию

Подписка с кастомным groupId

Для параллельных прогонов — уникальная группа на сценарий

Назначение всех партиций

Чтение с начала без участия в consumer group

Чтение (When / Когда)

Категория шага

Описание

Чтение всех сообщений

С настраиваемым таймаутом в секундах

Чтение одного сообщения

Первое попавшееся за таймаут

Фильтр по подстроке

Поиск среди накопленных сообщений

Очистка буфера

Сброс накопленных сообщений

Проверки (Then / Тогда)

Категория шага

Описание

Наличие сообщений

assertNotEmpty

Отсутствие сообщений

assertEmpty

Точное количество

assertEquals

Количество больше N

assertTrue(size > N)

Поиск подстроки в сообщениях

contains() по value

Проверка последнего сообщения

Проверка самого свежего сообщения

Сохранение в контекст

Значение последнего сообщения сохраняется для использования в последующих шагах

Паттерны использования

Паттерн 1: Подписка ДО триггерного действия

→ Выполняются предусловия → Подключение к Kafka → Выполняется триггерное действие → Проверка сообщения

Ключевая идея: consumer подключается до действия, которое порождает событие, чтобы гарантированно захватить сообщение.

Паттерн 2: Подписка ПОСЛЕ триггерного действия

→ Выполняются предусловия → Выполняется триггерное действие → Подключение к Kafka → Дополнительное действие → Проверка сообщения

Когда использовать: когда событие отправляется не сразу, а после дополнительного действия пользователя.

Паттерн 3: Негативная проверка — сообщений быть не должно

→ Выполняются предусловия → Подключение к Kafka → Выполняется действие → Проверка что сообщений НЕТ

Негативная проверка: определённые действия не должны порождать событий в Kafka.

5. Внешние зависимости → прямое подключение

Kafka REST Proxy, kcat, Confluent Control Center — все эти инструменты тянут за собой инфраструктуру, контейнеры, поддержку. В корпоративной среде это особенно критично: каждый новый сервис — это заявка на доступ, настройка SSL, синхронизация версий.

Коннектор использует прямое подключение через kafka-clients — одна зависимость в build.gradle, никаких контейнеров и прокси. Работает локально, в CI/CD, в Docker — достаточно указать bootstrapServers и креды.

Это даёт:

  • Минимальные зависимости — одна строка в build.gradle;

  • Максимальный контроль — фильтрация, таймауты, режимы подключения — всё настраивается программно;

  • Работает везде — локально, в CI/CD, в Docker;

  • Интеграция с отчётами — каждое действие логируется, в отчёте виден полный путь сообщения от топика до проверки.

Интеграция с отчётами

Каждый ключевой метод аннотирован @Step и добавляет вложения в отчёт:

@Step("Подписка на Kafka топик: {topic}")
public void subscribe(String topic) { ...
	Allure.addAttachment("Kafka подписка", "Подписка на топик: " + topic + ", groupId: " + groupId);
}
 
@Step("Чтение одного сообщения из Kafka топика")
public ConsumerRecord<String, String> pollOneMessage(int timeoutSeconds) { ...
	Allure.addAttachment("Kafka сообщение",
    	"Топик: " + record.topic() + ", партиция: " + record.partition() +
 	   ", offset: " + record.offset() + ", key: " + record.key() + ", value: " + record.value());
}

В отчёте видно: какой топик, какой groupId, сколько сообщений прочитано, и полное содержание каждого сообщения.

Контекст сценария — как связываются компоненты

Контекст сценария — Singleton на основе HashMap<String, Object>. Kafka использует 3 ключа:

Ключ

Тип

Кто записывает

Кто читает

Экземпляр consumer'а

Consumer-обёртка

BDD-шаги подключения

Хук закрытия после сценария

Список сообщений

List<ConsumerRecord>

Методы чтения и фильтрации

Все проверочные шаги

Последнее сообщение

ConsumerRecord

Метод чтения одного сообщения

Шаги проверки и сохранения в контекст

Скорость разработки: неделя за один день

Оценка ручной разработки такого коннектора «с нуля» — около недели: изучение SASL/SCRAM-конфигурации, реализация SslEngineFactory, написание consumer с poll-циклом, BDD-шаги, интеграция с контекстом сценария и хуками, отладка SSL-соединений, аннотации для отчётности.

С AI-ассистентом рабочее решение, прошедшее ревью, было готово за один рабочий день. Формат работы:

  1. Инженер определяет архитектурные решения: режимы подключения, модель данных, конфигурация, сценарии использования;

  2. AI-ассистент генерирует код: consumer-обёртку, кастомную SSL-фабрику, BDD-шаги, интеграцию с контекстом и хуками;

  3. Инженер проверяет, корректирует и запускает — итерация за итерацией.

Это не «AI написал код, а я нажал кнопку». Это партнёрская модель: инженер отвечает за архитектуру, решения и качество, AI-агент — за скорость генерации кода, исследование альтернатив и рутинную реализацию. Результат — неделя работы за один день, при этом качество не ниже, а в некоторых аспектах выше (например, SslEngineFactory с первого раза — без багов и переделок).

Инженер + AI-агент: модель взаимодействия

Коннектор был реализован при участии AI-ассистента по архитектурным требованиям и дизайн-решениям, подготовленным инженером по автоматизации. Но важно понимать: это не разовая история про «один коннектор». Это воспроизводимая модель работы, которую можно применить к любой задаче тестовой автоматизации.

Как это работает

Этап

Инженер

AI-агент

Поиск точек входа

Видит проблему, которую никто не оцифровывал: «мы проверяем UI, но не проверяем события»

Помогает исследовать: анализирует код, документацию, предлагает варианты решения

Архитектура

Принимает решения: режимы подключения, модель данных, интеграция с существующей инфраструктурой

Генерирует прототипы, сравнивает подходы, подсвечивает риски

Реализация

Ревьюит, корректирует, принимает решения о компромиссах

Пишет код: классы, шаги, конфигурация, интеграция

Отладка

Анализирует логи, принимает решения об изменениях

Исследует ошибки, предлагает фиксы, генерирует тестовые сценарии

Документирование

Определяет, что нужно задокументировать

Генерирует документацию, статьи, описания шагов

Почему это работает

  1. Инженер не заменён — он усилен. AI-агент не принимает архитектурных решений и не несёт ответственности за качество. Но он снимает рутину и ускоряет итерации в разы.

  2. Точки входа — не только в коде. Инженер, который ищет точки входа для автоматизации, находит их везде: в ручных проверках, в пропущенных багах, в медленных процессах. AI-агент помогает оцифровать эти точки и превратить их в работающие решения.

  3. Масштабируемость модели. Kafka-коннектор — один пример. Тот же подход работает для API-клиентов, DB-коннекторов, CI/CD-пайплайнов, отчётности. Если инженер видит точку входа — AI-агент помогает реализовать решение за часы, а не недели.

Результат

Параметр

Традиционный подход

Инженер + AI-агент

Время от идеи до рабочего решения

Недели

Дни

Качество кода

Зависит от опыта инженера

Выше за счёт мгновенного ревью и итераций

Покрытие проблем

Только то, на что хватило времени

Все точки входа, которые нашёл инженер

Стоимость

Часы инженера × недели

Часы инженера × дни

Заключение

Статья подготовлена на основе реального опыта разработки Kafka-коннектора для тестовой автоматизации. Если вы видите в своих процессах точки входа, где ручной труд можно заменить автоматизацией, а автоматизацию усилить AI, — эта модель сработает.

P.S. Если остались какие-то вопросы — добро пожаловать в комментарии, отвечу на все!