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