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

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

Привет, Хабр! Меня зовут Александр, я из Сбера, лидер по автоматизации в Департаменте Сервисы и Безопасности. В тестировании я около 13 лет, и последние лет 10 занимаюсь автоматизацией и её развитием в своём подразделении.

В этой статье расскажу, как с помощью IDE, LLM и RAG‑подхода можно автоматизировать одну из самых рутинных задач автоматизаторов — разработку новых автотестов по ручным сценариям, и при этом сохранять стиль и архитектуру проекта.

Где мы сейчас: ИИ уже в разработке, но почти не в автотестах

По даннымнедавнего опроса StackOverflow, 84% разработчиков постоянно используют ИИ‑ассистенты в своей работе. Для них это такой же привычный инструмент, как IDE или Git.

Как обычно работает подобный ИИ‑помощник в IDE:

  • анализирует открытые файлы;
  • дополнительно смотрит на соседние файлы в проекте;
  • учитывает локальный контекст: какие вопросы вы задавали и какой код меняли.

А вот автотестировщики часто оказываются как будто «на обочине» этой ИИ‑революции.

А как же автотесты?

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

Представим типичный проект с автотестами: есть класс с тестами на JUnit 5, класс с шагами и класс с утилитами. Автотесты пишем на Java, в JetBrains IDE, используем Allure для отчётности. Типичный API‑тест у нас выглядит так:

Здесь есть:

  • ключ ручного теста из TMS;
  • любимый Allure;
  • общий метод проверки кода ответа.

Автотесты — это тоже код. Часто не проще, а сложнее боевого приложения, и обладают теми же свойствами:

  • могут иметь сложную архитектуру (например,PageObjectдля UI);
  • требуют поддержки и рефакторинга;
  • живут в Git‑репозитории, часто со своим CI.

Постановка задачи: из ручного теста в автотест

Допустим, к нам на автоматизацию пришёл новый ручной тест:

Действие:отправить POST‑запрос по адресуhttps://jsonplaceholder.typicode.com/postsТестовые данные:{"title":"foo","body":"bar","userId":1}Ожидаемый результат:код ответа 200

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

  1. Вспоминает, есть ли похожие тесты.
  2. Смотрит, как в этом проекте принято писать тесты.
  3. Пишет новый тест в стиле проекта.

Раз уж у разработчиков есть ИИ‑ассистенты, то давайте попробуем сделать то же самое для автотестов: возьмём LLM, попросим её «превратить» ручной тест в автотест и посмотрим, что получится.

Пример автотеста, который может сгенерировать модель:

Код, скорее всего, даже заработает. Но:

  • нет Allure;
  • нарушен стиль наименования тестов;
  • URL и тело запроса инициализируются «не по‑нашему»;
  • используется стандартная проверка статуса вместо нашегоcheckCode;
  • модель добавила лишние проверки, которых не было в ручном тесте.

Почему «прямая» генерация не работает

Наивный подход «отправить текст теста в LLM и взять результат» плохо подходит для реальных проектов:

  • Интеграция дольше, чем ручная разработка.Нужно выкинуть лишнее, дописать нужное, подогнать под стиль и инфраструктуру.
  • Модель не знает контекст проекта.Фреймворки, обёртки, базовые утилиты, соглашения по именованию — всё это теряется.
  • Автотесты — не «обычный» код.Свои фреймворки, паттерны и ограничения.

Допустим, мы решили: «Ок, давайте отдадим модели весь проект: классы, шаги, утилиты, тесты». Сразу упираемся в технические ограничения:

  • В контекстное окно не помещается весь код, даже если его нарезать.
  • Запросы обрабатываются долго, и обогащение контекста тоже.
  • При использовании популярных облачных моделей это ещё и дорого.
  • Разворачивать у себя большую open source‑модель — дорого по «железу».

Плюс проблемы качества:

  • Галлюцинации (комментарии вместо кода, пропуск шагов).
  • Потеря контекста при больших объёмах.
  • Нестабильный результат: иногда нужно несколько перегенераций.

Few-shot: умнее, но всё ещё недостаточно

Следующий шаг — перейти от «наивной» генерации к few‑shot: показывать модели примеры уже существующих тестов и шагов.

Идея:даём модели только релевантные примеры из нашего проекта, и она генерирует тест «в нашем стиле».Разобьём промпт на две части.

Системный промпт:

  • роль модели;
  • задача;
  • формат входных и выходных данных.

Модель генерирует, например, такой тест:

Почти идеально:

  • появился вызов Allure;
  • ссылка на сервис там, где нужно;
  • имя теста соответствует нашему паттерну;
  • используется нашcheckCode;
  • никаких лишних проверок.

Но у статичного few‑shot есть фундаментальный недостаток:плохая масштабируемость.В реальном проекте — тысячи тестов. Для каждого нового ручного теста надо вручную подбирать набор «примеров», чтобы они были релевантны. Это долго и плохо автоматизируется.

Переходим к RAG: динамический поиск примеров

Решение — перейти от статичных примеров к динамическомуRAG‑подходу (Retrieval Augmented Generation). Идея:

  • Заранее сделать базу знаний по проекту.
  • По входящему ручному тестуискать по смыслурелевантные шаги и тесты;
  • Подставлять найденные примеры в промпт автоматически;
  • На выходе получать автотест «как будто его писал человек в этом проекте».

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

  1. Сформировать базу знаний о проекте.
  2. Настроить семантический поиск по этой базе.
  3. Интегрировать всё это в IDE, чтобы генерация была «в один клик».

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

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

  • Эмбеддинг— преобразование текста в числовой вектор. Специальная модель‑энкодер разбивает текст, анализирует значения и связи между словами и «упаковывает» смысл в вектор.
  • Метаданные— произвольные данные, которые мы храним рядом с вектором: код шага, место его вызова, файл, имя метода и т. п.
  • Векторное хранилище— база, которая умеет:хранить векторы и метаданные;быстро искать «похожие» векторы по косинусному расстоянию или другой метрике.

Общий план:

  1. Просканировать проект в IDE.
  2. Собранные данные (шаги, тесты) превратить в векторы.
  3. Сохранить в векторную базу.

Почему всё это встраиваем в IDE

Чтобы сканирование и работа с кодом были удобными и точными, мы делаем это в виде плагина для JetBrains IDE. IDE видит проект не как «текст», а как структуру PSI.

Упрощённо структура выглядит так:

  • PsiElement— базовый элемент (любая сущность или знак в файле);
  • PsiAnnotation— любая аннотация у метода;
  • PsiMethod— сам метод, включая аннотации, тело, комментарии;
  • PsiReference/PsiReferenceExpression— ссылки на методы (то, что мы видим поCtrl+Click);
  • ElementVisitor— «сканер», который рекурсивно обходит дерево PSI‑элементов.

Это даёт нам точный контроль над тем, что мы ищем и как это сохраняем.

Сканируем проект: сначала шаги

Мы хотим собрать:

  • все шаги Allure (по аннотации@Step);
  • все вызовыAllure.step(...).

Пример теста:

  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);
  • код теста;
  • краткое текстовое описание (для семантического поиска).

Алгоритм индексации:

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

  1. Передаём код теста в LLM с простым запросом «Объясни кратко этот тест».
  2. Получаем лаконичное текстовое описание, например: «Проверка отправки GET‑запроса по адресу с проверкой успешного ответа HTTP‑статуса 200».
  3. Именно это описание используем для эмбеддинга и семантического поиска.

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

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

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

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

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

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

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

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

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

Поиск шагов

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

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

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

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

Поиск тестов

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

  1. Получаем от LLM краткое описание ручного теста.
  2. Эмбеддим это описание.
  3. Ищем в базе эмбендингов существующий автотест с ближайшим по смыслу описанием.

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

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

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

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

  1. Проверка LLM‑результата самой моделью.Делаем дополнительный запрос: отдаём сгенерированный код и просим модель перепроверить результат по нашим правилам.
  2. Проверка на уровне IDE через PSI.Здесь мы уже программно убеждаемся, что:
  • Синтаксис Java корректный;
  • Нужные аннотации на месте;
  • Ключи тестов и другие чувствительные данные не потерялись и не «исказились».

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

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

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

  • Действие:отправить POST‑запрос кhttps://jsonplaceholder.typicode.com/posts
  • Тестовые данные:{"title":"foo","body":"bar","userId":1}
  • Ожидаемый результат:код ответа 200
  1. Разбиваем шаг на три текстовых варианта:«Отправить POST‑запрос»;«Код ответа 200»;«Отправить 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. Сразу предупрежу: там нет всего кода, но есть основа для старта

Читать оригинал