
Привет, Хабр! Меня зовут Александр, я из Сбера, лидер по автоматизации в Департаменте Сервисы и Безопасности. В тестировании я около 13 лет, и последние лет 10 занимаюсь автоматизацией и её развитием в своём подразделении.
В этой статье расскажу, как с помощью IDE, LLM и RAG‑подхода можно автоматизировать одну из самых рутинных задач автоматизаторов — разработку новых автотестов по ручным сценариям, и при этом сохранять стиль и архитектуру проекта.
Где мы сейчас: ИИ уже в разработке, но почти не в автотестах
По данным недавнего опроса StackOverflow, 84% разработчиков постоянно используют ИИ‑ассистенты в своей работе. Для них это такой же привычный инструмент, как IDE или Git.
Как обычно работает подобный ИИ‑помощник в IDE:
анализирует открытые файлы;
дополнительно смотрит на соседние файлы в проекте;
учитывает локальный контекст: какие вопросы вы задавали и какой код меняли.
А вот автотестировщики часто оказываются как будто «на обочине» этой ИИ‑революции.
А как же автотесты?
Про них зачастую вспоминают в последнюю очередь: «Главное, чтобы разработка шла быстро!» При этом очевидная зависимость — чем больше кода, тем больше нужно тестов — почему-то чаще всего волнует только тестировщиков.
Представим типичный проект с автотестами: есть класс с тестами на JUnit 5, класс с шагами и класс с утилитами. Автотесты пишем на Java, в JetBrains IDE, используем Allure для отчётности. Типичный API‑тест у нас выглядит так:
@Test @TmsLink("MYKEY-T1") public void getTest() { Response resp = Allure.step("Отправка GET запроса", () -> RestAssured.given() .baseUri("https://jsonplaceholder.typicode.com") .basePath("todos/1") .get()); checkCode(resp); } @Step("Проверка кода статуса") public void checkCode(Response resp) { // code }
Здесь есть:
ключ ручного теста из TMS;
любимый Allure;
общий метод проверки кода ответа.
Автотесты — это тоже код. Часто не проще, а сложнее боевого приложения, и обладают теми же свойствами:
могут иметь сложную архитектуру (например,
PageObjectдля UI);требуют поддержки и рефакторинга;
живут в Git‑репозитории, часто со своим CI.
Постановка задачи: из ручного теста в автотест
Допустим, к нам на автоматизацию пришёл новый ручной тест:
Действие: отправить POST‑запрос по адресу
https://jsonplaceholder.typicode.com/postsТестовые данные:
{"title":"foo","body":"bar","userId":1}Ожидаемый результат: код ответа 200
Что делает обычный автоматизатор?
Вспоминает, есть ли похожие тесты.
Смотрит, как в этом проекте принято писать тесты.
Пишет новый тест в стиле проекта.
Раз уж у разработчиков есть ИИ‑ассистенты, то давайте попробуем сделать то же самое для автотестов: возьмём LLM, попросим её «превратить» ручной тест в автотест и посмотрим, что получится.
Пример автотеста, который может сгенерировать модель:
@Test public void testCreatePostShouldReturnStatusCode200() { RestAssured.baseURI = "https://jsonplaceholder.typicode.com"; String requestBody = "{\"title\":\"foo\",\"body\":\"bar\",\"userId\":1\"}"; // code .statusCode(200) .contentType(ContentType.JSON) .body("title", equalTo("foo")) .body("body", equalTo("bar")) .body("userId", equalTo(1)); }
Код, скорее всего, даже заработает. Но:
нет Allure;
нарушен стиль наименования тестов;
URL и тело запроса инициализируются «не по‑нашему»;
используется стандартная проверка статуса вместо нашего
checkCode;модель добавила лишние проверки, которых не было в ручном тесте.
Почему «прямая» генерация не работает
Наивный подход «отправить текст теста в LLM и взять результат» плохо подходит для реальных проектов:
Интеграция дольше, чем ручная разработка. Нужно выкинуть лишнее, дописать нужное, подогнать под стиль и инфраструктуру.
Модель не знает контекст проекта. Фреймворки, обёртки, базовые утилиты, соглашения по именованию — всё это теряется.
Автотесты — не «обычный» код. Свои фреймворки, паттерны и ограничения.
Допустим, мы решили: «Ок, давайте отдадим модели весь проект: классы, шаги, утилиты, тесты». Сразу упираемся в технические ограничения:
В контекстное окно не помещается весь код, даже если его нарезать.
Запросы обрабатываются долго, и обогащение контекста тоже.
При использовании популярных облачных моделей это ещё и дорого.
Разворачивать у себя большую open source‑модель — дорого по «железу».
Плюс проблемы качества:
Галлюцинации (комментарии вместо кода, пропуск шагов).
Потеря контекста при больших объёмах.
Нестабильный результат: иногда нужно несколько перегенераций.
Few-shot: умнее, но всё ещё недостато��но
Следующий шаг — перейти от «наивной» генерации к few‑shot: показывать модели примеры уже существующих тестов и шагов.
Идея: даём модели только релевантные примеры из нашего проекта, и она генерирует тест «в нашем стиле». Разобьём промпт на две части.
Системный промпт:
роль модели;
задача;
формат входных и выходных данных.
Пример:
# Роль Ты — Senior Java QA Automation Engineer. ## Твоя задача Выполнять преобразование шагов ручного теста в Java-код с Allure-аннотациями. Пользовательский промпт: - примеры похожих шагов; - примеры похожих тестов; - новый ручной тест, который нужно автоматизировать. ## Примеры похожих шагов Используй существующие allure-методы тестового фреймворка Проверка кода статуса => checkCode(resp) ## Примеры похожих тестов Используй примеры тестов: @Test @TmsLink("MYKEY-T1") public void getTest() { // code } ## Ручной тест Преобразуй ручной тест в автотест. Действие: Отправить POST-запрос по адресу: https://jsonplaceholder.typicode.com/posts Тестовые данные: {"title":"foo","body":"bar","userId":1} Ожидаемый результат: код ответа 200
Модель генерирует, например, такой тест:
@Test @TmsLink("MYKEY-T2") public void test_MYKEY_T2() { Response resp = Allure.step("Отправить POST-запрос по адресу", () -> { RestAssured.given() .baseUri("https://jsonplaceholder.typicode.com") .basePath("posts") .body("{\"title\":\"foo\",\"body\":\"bar\",\"userId\":1}") .when() .post(); }); checkCode(resp); }
Почти идеально:
появился вызов Allure;
ссылка на сервис там, где нужно;
имя теста соответствует нашему паттерну;
используется наш
checkCode;никаких лишних проверок.
Но у статичного few‑shot есть фундаментальный недостаток: плохая масштабируемость. В реальном проекте — тысячи тестов. Для каждого нового ручного теста надо вручную подбирать набор «примеров», чтобы они были релевантны. Это долго и плохо автоматизируется.
Переходим к RAG: динамический поиск примеров
Решение — перейти от статичных примеров к динамическому RAG‑подходу (Retrieval Augmented Generation). Идея:
Заранее сделать базу знаний по проекту.
По входящему ручному тесту искать по смыслу релевантные шаги и тесты;
Подставлять найденные примеры в промпт автоматически;
На выходе получать автотест «как будто его писал человек в этом проекте».
Чтобы это работало, нам нужно:
Сформировать базу знаний о проекте.
Настроить семантический поиск по этой базе.
Интегрировать всё это в IDE, чтобы генерация была «в один клик».
Немного теории: эмбеддинги, метаданные и векторное хранилище
Основные понятия:
Эмбеддинг — преобразование текста в числовой вектор. Специальная модель‑энкодер разбивает текст, анализирует значения и связи между словами и «упаковывает» смысл в вектор.
Метаданные — произвольные данные, которые мы храним рядом с вектором: код шага, место его вызова, файл, имя метода и т. п.
Векторное хранилище — база, которая умеет:
хранить векторы и метаданные;
быстро искать «похожие» векторы по косинусному расстоянию или другой метрике.
Общий план:
Просканировать проект в IDE.
Собранные данные (шаги, тесты) превратить в векторы.
Сохранить в векторную базу.
Почему всё это встраиваем в IDE
Чтобы сканирование и работа с кодом были удобными и точными, мы делаем это в виде плагина для JetBrains IDE. IDE видит проект не как «текст», а как структуру PSI.
Упрощённо структура выглядит так:
PsiElement ├─ PsiFile ├─ PsiClass ├─ PsiMethod ├─ PsiAnnotation └─ PsiReference
PsiElement— базовый элемент (любая сущность или знак в файле);PsiAnnotation— любая аннотация у метода;PsiMethod— сам метод, включая аннотации, тело, комментарии;PsiReference/PsiReferenceExpression— ссылки на методы (то, что мы видим поCtrl+Click);ElementVisitor— «сканер», который рекурсивно обходит дерево PSI‑элементов.
Это даёт нам точный контроль над тем, что мы ищем и как это сохраняем.
Сканируем проект: сначала шаги
Мы хотим собрать:
все шаги Allure (по аннотации
@Step);все вызовы
Allure.step(...).
Пример теста:
@Test @TmsLink("MYKEY-T1") public void getTest() { Response resp = Allure.step("Отправка GET запроса", // code ); } @Step("Проверка кода статуса") public void checkCode(int code) { // code }
Подход:
Ищем класс аннотации шага:
PsiClass stepAnnotation = findClass("io.qameta.allure.Step");Находим все методы с этой аннотацией и все их использования (
PsiReference).Отдельно проходимся по проекту
JavaRecursiveElementVisitorи ищем статические вызовыAllure.step(...):psiJavaFile.accept(new JavaRecursiveElementVisitor() { @Override public void visitMethodCallExpression(PsiMethodCallExpression expression) { // Проверяем: "Allure.step"? } });Для static‑шагов описанием считаем первый параметр
Allure.step("описание шага", ...), а примером использования — сам вызов.
Сканируем проект: теперь тесты
Шаги собрали, теперь нужны сами автотесты. Обычно тестов больше, чем шагов, поэтому для быстроты используем механизмы индексации IDE. Мы хотим для каждого теста:
ключ в TMS (например,
"MYKEY-T1"из@TmsLink);код теста;
краткое текстовое описание (для семантического поиска).
Пример:
@Test @TmsLink("MYKEY-T1") public void getTest() { Response resp = Allure.step("Отправка GET запроса", () -> RestAssured.given() .baseUri("https://jsonplaceholder.typicode.com") .basePath("todos/1") .get() ); // code }
Алгоритм индексации:
psiFile.accept(new JavaRecursiveElementVisitor() { @Override public void visitMethod(PsiMethod method) { if (isTest(method)) { String key = getTestKey(method); // достаём, например, из @TmsLink result.put(key, method); } } });
С кодом теста есть проблема: он не является его «смысловым» описанием. Поэтому мы:
Передаём код теста в LLM с простым запросом «Объясни кратко этот тест».
Получаем лаконичное текстовое описание, например: «Проверка отправки GET‑запроса по адресу с проверкой успешного ответа HTTP‑статуса 200».
Именно это описание используем для эмбеддинга и семантического поиска.
Эмбеддинг и хранение
Что мы эмбеддим:
для каждого шага — его текстовое описание;
для каждого теста — краткое текстовое описание.
В метаданные кладём:
исходный код шага/теста;
место в проекте;
Для прототипа достаточно хранить всё в памяти (или в файле между перезапусками IDE). Для серьёзного решения лучше использовать специализированную векторную БД.
Алгоритм поиска примеров
База готова, теперь нужно научиться по новому ручному тесту находить подходящие шаги и тесты. Разобьём задачу на два этапа:
поиск шагов;
поиск тестов.
Поиск шагов
Ручной тест может состоять из множества шагов. Минимально каждый из них — это действие и ожидаемый результат.
Чтобы повысить шанс найти что‑то похожее, для каждого шага делаем три текстовые комбинации:
только действие;
только ожидаемый результат;
действие + ожидаемый результат.
Каждую комбинацию эмбеддим и ищем ближайшие по смыслу шаги в ранее созданной базе эмбендингов.
Поиск тестов
С тестами сложнее: сумма всех шагов — это не «смысл» теста. Текст шагов отличается от краткого описания. Поэтому:
Получаем от LLM краткое описание ручного теста.
Эмбеддим это описание.
Ищем в базе эмбендингов существующий автотест с ближайшим по смыслу описанием.
При поиске учитываем степень сходства (score). Если, например, ищем «Проверить код 200», а находим четыре подходящих шага, то используем этот score, чтобы выбрать лучшего кандидата.
Финальный запрос к LLM
Когда мы нашли подходящие шаги и тесты, остаётся правильно сформировать запрос к модели. Вместо статичных блоков few‑shot подставляем параметры, которые заполняются автоматически:
## Примеры похожих шагов Используй существующие allure-методы тестового фреймворка {{stepExamples}} ## Примеры похожих тестов Используй примеры тестов {{testExamples}} ## Ручной тест Преобразуй ручной тест в автотест {{stepsForManualTest}}
Дальше есть две важные проверки:
Проверка LLM‑результата самой моделью. Делаем дополнительный запрос: отдаём сгенерированный код и просим модель перепроверить результат по нашим правилам.
Проверка на уровне IDE через PSI. Здесь мы уже программно убеждаемся, что:
Синтаксис Java корректный;
Нужные аннотации на месте;
Ключи тестов и другие чувствительные данные не потерялись и не «исказились».
После этого вставляем проверенный код в редактор IDE.
Пример полного цикла на новом тест
Вернёмся к нашему ручному тесту:
Действие: отправить POST‑запрос к
https://jsonplaceholder.typicode.com/postsТестовые данные:
{"title":"foo","body":"bar","userId":1}Ожидаемый результат: код ответа 200
Разбиваем шаг на три текстовых варианта:
«Отправить POST‑запрос»;
«Код ответа 200»;
«Отправить POST‑запрос + код ответа 200».
Для этих вариантов ищем похожие шаги в базе и находим, например, метод
checkCode.Генерируем краткое описание ручного теста и по нему ищем похожий автотест.
Подставляем найденные шаги и тесты в шаблон промпта.
Отправляем запрос LLM, проверяем результат моделью и через PSI, вставляем код в IDE.
Ключевая идея: RAG делает автоматически то, что мы делали бы руками с few‑shot, только:
учитывает весь проект;
масштабируется на тысячи тестов;
экономит время автоматизатора.
По сути, RAG — это few‑shot, который сам находит нужные примеры.
Наш прототип и результаты
Мы реализовали описанное решение в виде внутреннего прототипа. По отзывам пользователей:
68% сгенерированных тестов получились на приемлемом уровне и требовали
минимальных правок.Общая удовлетворённость — около 80%: людям в целом понравилась идея генерации автотестов прямо из IDE.
Пользователи особенно отметили:
упрощение написания простых и похожих тестов;
сохранение стиля проекта;
снижение когнитивной нагрузки — можно сосредоточиться на сложных сценариях и архитектуре тестов.
Недостатки:
Галлюцинации пока никуда не делись — любая LLM работает на вероятностях.
Сложные тесты (несколько действий и ожидаемых результатов в одном шаге, большие сценарии на 20+ шагов) даются тяжелее: модель начинает упрощать и сокращать.
Прототип лучше всего показал себя на API‑тестах, а для UI‑тестов требуется больше контекста (информация об объектах страницы).
При этом:
Серьёзных ограничений по языкам мы не увидели: результаты для Java, Python и Gherkin были сопоставимы.
Главное — качественно собранная база знаний и настройка промптов под конкретный фреймворк и язык.
Выводы

Всегда проверяйте результат. Любая модель в роли ассистента — это помощник, а не замена автоматизатору.
Качество генерации зависит от качества тестов. Чем чище код автотестов и чем техничнее написаны ручные сценарии, тем лучше работает связка LLM и RAG.
Подход реально экономит время. По нашим оценкам, такой инструмент позволяет сэкономить более половины времени автоматизатора на рутинной разработке новых тестов.
Если вам интересны технические подробности реализации плагина или, например, интеграция с конкретной векторной БД, — напишите в комментариях, это можно разобрать в отдельной статье.
Прототип такого плагина с генерацией с локальным RAG можете попробовать развернуть у себя https://gitverse.ru/Sergo01/llm-demo-plugin. Сразу предупрежу: там нет всего кода, но есть основа для старта
