Автогенерация тестов в IDE: как RAG + LLM превращают ручные сценарии в код
Автогенерация тестов в IDE: как RAG + LLM превращают ручные сценарии в код

Привет, Хабр! Меня зовут Александр, я из Сбера, лидер по автоматизации в Департаменте Сервисы и Безопасности. В тестировании я около 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

Что делает обычный автоматизатор? 

  1. Вспоминает, есть ли похожие тесты.

  2. Смотрит, как в этом проекте принято писать тесты.

  3. Пишет новый тест в стиле проекта.

Раз уж у разработчиков есть ИИ‑ассистенты, то давайте попробуем сделать то же самое для автотестов: возьмём 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). Идея:

  • Заранее сделать базу знаний по проекту.

  • По входящему ручному тесту искать по смыслу релевантные шаги и тесты;

  • Подставлять найденные примеры в промпт автоматически;

  • На выходе получать автотест «как будто его писал человек в этом проекте».

Чтобы это работало, нам нужно:

  1. Сформировать базу знаний о проекте.

  2. Настроить семантический поиск по этой базе.

  3. Интегрировать всё это в IDE, чтобы генерация была «в один клик».

Немного теории: эмбеддинги, метаданные и векторное хранилище

Основные понятия:

  • Эмбеддинг — преобразование текста в числовой вектор. Специальная модель‑энкодер разбивает текст, анализирует значения и связи между словами и «упаковывает» смысл в вектор.

  • Метаданные — произвольные данные, которые мы храним рядом с вектором: код шага, место его вызова, файл, имя метода и т. п.

  • Векторное хранилище — база, которая умеет:

    • хранить векторы и метаданные;

    • быстро искать «похожие» векторы по косинусному расстоянию или другой метрике.

Общий план:

  1. Просканировать проект в IDE.

  2. Собранные данные (шаги, тесты) превратить в векторы.

  3. Сохранить в векторную базу.

Почему всё это встраиваем в 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
}

Подход: 

  1. Ищем класс аннотации шага:

    PsiClass stepAnnotation = findClass("io.qameta.allure.Step");
  2. Находим все методы с этой аннотацией и все их использования (PsiReference).

  3. Отдельно проходимся по проекту 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);
        }
    }
});

С кодом теста есть проблема: он не является его «смысловым» описанием. Поэтому мы:

  1. Передаём код теста в LLM с простым запросом «Объясни кратко этот тест».

  2. Получаем лаконичное текстовое описание, например: «Проверка отправки GET‑запроса по адресу с проверкой успешного ответа HTTP‑статуса 200».

  3. Именно это описание используем для эмбеддинга и семантического поиска.

Эмбеддинг и хранение

Что мы эмбеддим:

  • для каждого шага — его текстовое описание;

  • для каждого теста — краткое текстовое описание.

В метаданные кладём:

  • исходный код шага/теста;

  • место в проекте;

Для прототипа достаточно хранить всё в памяти (или в файле между перезапусками IDE). Для серьёзного решения лучше использовать специализированную векторную БД.

Алгоритм поиска примеров

База готова, теперь нужно научиться по новому ручному тесту находить подходящие шаги и тесты. Разобьём задачу на два этапа:

  1. поиск шагов;

  2. поиск тестов.

Поиск шагов

Ручной тест может состоять из множества шагов. Минимально каждый из них — это действие и ожидаемый результат.

Чтобы повысить шанс найти что‑то похожее, для каждого шага делаем три текстовые комбинации:

  1. только действие;

  2. только ожидаемый результат;

  3. действие + ожидаемый результат.

Каждую комбинацию эмбеддим и ищем ближайшие по смыслу шаги в ранее созданной базе эмбендингов.

Поиск тестов

С тестами сложнее: сумма всех шагов — это не «смысл» теста. Текст шагов отличается от краткого описания. Поэтому:

  1. Получаем от LLM краткое описание ручного теста.

  2. Эмбеддим это описание.

  3. Ищем в базе эмбендингов существующий автотест с ближайшим по смыслу описанием.

При поиске учитываем степень сходства (score). Если, например, ищем «Проверить код 200», а находим четыре подходящих шага, то используем этот score, чтобы выбрать лучшего кандидата.

Финальный запрос к LLM

Когда мы нашли подходящие шаги и тесты, остаётся правильно сформировать запрос к модели. Вместо статичных блоков few‑shot подставляем параметры, которые заполняются автоматически:

## Примеры похожих шагов
Используй существующие allure-методы тестового фреймворка
{{stepExamples}}
 
## Примеры похожих тестов
Используй примеры тестов
{{testExamples}}
## Ручной тест
Преобразуй ручной тест в автотест
{{stepsForManualTest}}

Дальше есть две важные проверки:

  1. Проверка LLM‑результата самой моделью. Делаем дополнительный запрос: отдаём сгенерированный код и просим модель перепроверить результат по нашим правилам.

  2. Проверка на уровне IDE через PSI. Здесь мы уже программно убеждаемся, что:

  • Синтаксис Java корректный;

  • Нужные аннотации на месте;

  • Ключи тестов и другие чувствительные данные не потерялись и не «исказились».

 После этого вставляем проверенный код в редактор IDE.

Пример полного цикла на новом тест

Вернёмся к нашему ручному тесту: 

  • Действие: отправить POST‑запрос к https://jsonplaceholder.typicode.com/posts

  • Тестовые данные: {"title":"foo","body":"bar","userId":1}

  • Ожидаемый результат: код ответа 200

  1. Разбиваем шаг на три текстовых варианта:

    1. «Отправить POST‑запрос»;

    2. «Код ответа 200»;

    3. «Отправить POST‑запрос + код ответа 200».

  2. Для этих вариантов ищем похожие шаги в базе и находим, например, метод checkCode.

  3. Генерируем краткое описание ручного теста и по нему ищем похожий автотест.

  4. Подставляем найденные шаги и тесты в шаблон промпта.

  5. Отправляем запрос 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. Сразу предупрежу: там нет всего кода, но есть основа для старта

Комментарии (4)


  1. Robastik
    20.03.2026 08:23

    Подход потенциально галлюциногенный. Почему не сделать набор скиллов под разные типы тестов со своими правилами и шаблонами?


    1. Sergo_01 Автор
      20.03.2026 08:23

      Приветствую!

      Про подход согласен, как и все что делает LLM - может галлюцинировать. Но сильно зависит от качества исходных данных, то есть от проекта автотестов. Автоматизаторы люди творческие =)

      Про скилы - тоже интересная мысль, вполне можно сделать. Главный вопрос только сколько таких скилов под множество вариаций написания автотестов нужно будет реализовать. Или скилы должны писаться сами или каждый под себя свои варианты должны сделать.

      А тут история сделать что-то универсальное


      1. thethee
        20.03.2026 08:23

        Что-то универсальное это не узкопрофильный генератор автотестов, а полноценный кодовый агент, у которого есть скиллы работы с jira/confluence, релевантная кодовая база. И задача "напиши мне тест на то как открывается эта страничка" самим агентом декомпозируется в "посмотреть что страничка должна делать", "посмотреть выполнена ли задача на страничку", "изучить паттерны написания тестов в проекте" и только потом уже "написать тест".

        Поэтому выше вам и писали про скиллы, потому что это не условный чат делать должен, а именно кодовый агент - опенкод/клод/кодекс. С вашей моделью под капотом можно любой из этих инструментов настроить, и после правильных приготовлений у вас будет конфиг для автотестера на Java, конфиг для программиста Java, конфиг для программиста Python и так далее.


        1. Sergo_01 Автор
          20.03.2026 08:23

          Так я и не спорю. Я лишь описал свой опыт подойди к задаче по генерации автотестов.

          При этом, как мне кажется, скилы по типам тестов даже интереснее. GUI, REST, DB и далее. С учётом что моделям уже не так важен язык - они плюс/минус хороши во всех.