
Привет, Хабр. На связи Даниил Кобылкин (@danilkkk) и Тимофей Тимошевский (@timhok17). Мы фронтенд‑разработчики в Одноклассниках.
Юнит‑тесты — важный этап проверки качества кода, позволяющий ловить ошибки на ранней стадии и удешевлять сопровождение проекта. Но давайте честно: писать их вручную — скучно, долго и не всегда хочется. Поэтому в Одноклассниках мы решили избавиться от этой рутины и автоматизировать процесс генерации юнит‑тестов.
Рассказываем в статье, какие инструменты и подходы для генерации юнит‑тестов существуют, и как мы научились генерировать юнит‑тесты в своем проекте.
Немного контекста
ОК — большой, высоконагруженный проект, которым ежемесячно пользуется 36 млн человек. И наша соцсеть динамично развивается — мы катим релизы несколько раз в неделю.
В подобных условиях нам крайне важно гарантировать стабильность сервисов, при этом максимально оптимизировать рабочие процессы, чтобы сфокусировать доступные ресурсы разработчиков и тестировщиков на более важных задачах. Для проверки работы портала мы в основном используем e2e‑тесты, их у нас уже больше 7,5 тысяч.
Однако с юнит‑тестами ситуация была далека от идеальной — в какой‑то момент пирамида тестирования оказалась перевернутой: e2e‑тесты стали её основой, а вот юнит‑тесты ушли на второй план, что противоречит классическим принципам тестирования и делает тестирование неоптимальным.

Одна из причин заключается в том, что написание unit‑тестов не являлось жесткой необходимостью в рамках интенсивной продуктовой разработки. Это рутинный процесс, отнимающий много времени — когда задача уже выполнена, а в спринте еще пачка открытых тасок, редко кто хочет тратить время на юнит‑тесты, если код и так работает.
При этом стабильность сервисов — один из приоритетов нашей команды. Юнит‑тесты играют здесь ключевую роль, даже во фронтенде: они позволяют находить ошибки на ранней стадии, когда их исправление обходится минимальными усилиями, и помогают уверенно вносить изменения, не опасаясь сломать существующую логику. Мы поняли, что игнорировать такой мощный инструмент — недопустимая роскошь, и решили исправить дисбаланс в нашей тестовой пирамиде.
Но для этого сначала нужно было разобраться, сколько юнит‑тестов действительно нужно. Как понять, что их уже достаточно, или наоборот — что критически важные участки кода всё ещё остаются без проверки?
Определение достаточного количества unit-тестов
К счастью, существует объективный способ это оценить — метрика Code Coverage, или покрытие кода тестами. Термин давно известный, но нелишним будет напомнить, о чём идёт речь.
Проще говоря, Code Coverage показывает, какая часть исходного кода была выполнена во время прогона тестов. Чем выше этот показатель, тем больше уверенности, что тесты действительно проходят по всем важным веткам логики, и меньше вероятность, что в тени остались баги.
Есть несколько способов посчитать покрытие. Остановимся на самых популярных.
-
Line Coverage (покрытие строк). Подход, который учитывает процент строк кода, выполняемых при прогоне тестов.
function calculateLineExample() { const a = 1; // Строка 1 const b = 2; // Строка 2 return a + b; // Строка 3 }
-
Statement Coverage (покрытие операторов). Подход, который оценивает, сколько инструкций (операторов) в исходном коде было протестировано. В императивных языках программирования оператором называется самая малая часть программного кода, которая выполняет действие
function statementVSLineExample(arr) { const a = 1, b = 2; // Оператор 1-2 -> Строка 1 return arr.filter(x => x > a + b); // Оператор 3-7 -> Строка 2 }
Примечание: Statement Coverage имеет преимущество перед покрытием строк. Особенно в случае, когда язык использует много коротких утверждений в одной строке. Например, методы массивов или же объявление переменных могут содержать несколько операторов в одной строке.
-
Branch Coverage (покрытие ветвей). Подход, который измеряет процент покрытия ветвей (точек принятия решений) в исходном коде. Ветвями считаются:
условные операторы (
if/else
);switch / case / default
;различные логические операторы (
&&, ||
);циклы;
try … catch … finally
и другие.
-
Function Coverage (покрытие функций). Подход, который проверяет, была ли вызвана каждая функция/метод хотя бы раз. Не проверяет логику внутри функции — только факт использования.
// Функция 1 function add(a, b) { return a + b; } // Функция 2 function subtract(a, b) { return a - b; } it(‘add and subtract functions’, () => { expect(add(2, 3)).toBe(5); expect(subtract(5, 2)).toBe(3); });
Стоит отметить, что не существует универсального расчёта покрытия, который подходил бы для всех проектов. Зачастую выбор зависит от:
языка программирования (в функциональных языках важнее Function Coverage, в императивных — Statement/Branch);
критичности кода (например, в финансовых системах важнее Branch Coverage, нежели Line Coverage) и других параметров.
Существуют готовые инструменты, которые комбинируют разные методы анализа. Например, Cobertura сначала проверяет выполнение каждой строки кода (Line Coverage), а затем анализирует все возможные ветвления логики (Branch Coverage), что дает объективную оценку тестирования. Для выбора схожих инструментов можно воспользоваться сравнительными таблицами, одну из них мы оставим здесь.
Примечание: Важно понимать, что 100 % покрытие — это не гарантия идеального кода. Можно добиться полного покрытия, написав формальные тесты, которые ничего по сути не проверяют. Такие тесты создают лишь иллюзию надежности. Поэтому стремиться нужно не к цифрам ради цифр, а к осознанному покрытию — когда тесты действительно проверяют ключевую логику и помогают находить ошибки, а не просто «заполняют статистику».
От задачи к реализации
Итак, как всё же добиться заветного высокого покрытия в проекте? Вариантов немного.
Можно пойти простым путем — попросить ответственных разработчиков вручную дописать тесты к уже существующему коду. Но на практике это проблематично: мотивации немного, так как задача скучная. Такой подход и затратен, и не слишком эффективен.
Зато есть альтернатива, куда более перспективная — подключить к задаче большие языковые модели (LLM). Нейросети уже умеют генерировать код, учитывая контекст и структуру проекта, и могут взять на себя рутинную часть работы. То есть такой вариант в перспективе позволяет не только быстро гарантировать более высокую надежность, но и позаботиться о наших коллегах.
Здесь стоит немного остановиться на сути работы самих LLM.
Коротко об LLM
Современные LLM:
обучаются на больших данных;
содержат более 1 млрд параметров;
эффективно используются для перевода, прогноза, генерации текста/кода/медиаконтента и не только.
Параметры модели — это различные переменные, которые могут меняться в процессе обучения. Чем их больше, тем «умнее» и качественнее считается модель.
Результат и качество генерации LLM тесно связаны с двумя понятиями:
Промпты — запросы или инструкции, которые пользователь задает модели, чтобы получить желаемый ответ. Чаще всего промпт представляет собой текст, но есть возможность задать его и в формате медиа, например, в виде картинки. Чем лучше и подробнее будет составлен промпт, тем больше вероятность, что результат генерации вас удовлетворит. Подробнее про «best practice» написания промптов можно узнать здесь.
-
Контекст — информация, которую модель учитывает при генерации нового ответа, включая запрос (промпт), ранее сгенерированные ответы, системный промпт и различную метаинформацию — например, прикрепленные документы, изображения и другие данные, явно предоставленные пользователем. Если по простому — это агрегация всех ранее заданных промптов и результатов генерации.
Размер контекста ограничен числом токенов — последовательностей текстовых символов. Токеном может являться часть слова, слово или словосочетание целиком. Для разбиения текста на токены существуют различные инструменты, один из них tokenizer — он делает расчёт для OpenAI моделей.
Важно помнить, что если общий объём контекста превышает лимит токенов, то модель будет учитывать только часть приведенной информации, а это может негативно повлиять на точность и качество ожидаемого ответа.

Но при понимании особенностей кодировок эту проблему можно частично решить — задавать промпты на английском языке. Это обусловлено тем, что фразы на английском обычно более краткие.

То есть при общении с LLM на английском, меньше вероятность выйти за пределы контекста.
Помимо промптов и контекста также важно учитывать «креативность» модели, которой можно управлять, изменяя специальные параметры:
temperature
— параметр, который меняет форму всего распределения вероятностей следующего слова/токена (чем она выше, тем более креативные ответы генерирует модель);top‑p
— выборка наиболее вероятных слов, суммарная вероятность которых не превышает установленный p, тем самым отсекая редкие варианты;top‑k
— ограничивает количество наиболее вероятных слов (например,k=2
означает выбор из 2 самых вероятных слов);max token length
— максимальное количество токенов в сгенерированном ответе;stop sequences
— специальные последовательности символов или слов, при появлении которых генерация прерывается;frequency penalty
— штраф за повторение одних и тех же слов (помогает уменьшить количество повторов в тексте);presence penalty
— похож наfrequency
, однако здесь вводятся штрафы за повторное появление слов/токенов вообще, уводя генерацию в различные сторонние темы.

Примечание: Наряду с настройкой «креативности», также важно предоставлять LLM необходимый объем вычислительных мощностей — от этого тоже зависит качество генерации.
Как выбрать модель
Сегодня существует огромное количество языковых моделей, и выбрать подходящую для конкретного сценария — нетривиальная задача. Разные модели по‑разному справляются с генерацией кода, различаются по качеству, скорости и стоимости использования. Чтобы не выбирать вслепую, можно воспользоваться специализированными сервисами — например, LLM Arena.
LLM Arena — это платформа, которая позволяет сравнивать нейросети вживую: вы задаёте один и тот же вопрос нескольким моделям и сразу видите, как каждая из них отвечает. Это отличный способ устроить небольшой «тест‑драйв» перед тем, как определиться с выбором. Если вы хотите понять, какие модели сейчас на рынке действительно хороши — этот сервис будет полезен.
При выборе модели в первую очередь стоит обращать внимание на нефункциональные требования. Например, насколько хорошо модель справляется с генерацией кода, есть ли возможность локального развертывания — на ноутбуке или в облаке, — а также насколько затратными будут запросы к модели при активном использовании.
Соображения безопасности обязывают использовать локальные модели, которые будут располагаться во внутреннем контуре, поскольку публичные модели, такие как ChatGPT, обрабатывают данные на сторонних серверах. Это значит, что код и бизнес‑логика, которые могут содержать конфиденциальную информацию, покидают пределы компании — а для многих организаций, включая нас, это неприемлемо. К счастью, у нас есть VK Copilot, внутренний сервис VK, в котором уже развернуты модели для работы с текстами и кодом, что исключает риск утечки данных.
Подход к генерации тестов
Теперь остановимся на генерации тестов. В качестве примера рассмотрим кейс развертывания TestGen‑LLM.
TestGen‑LLM — система генерации тестов на собственных мощностях Meta*. Архитектура системы включает pre‑ и post‑process, но ее ключевым компонентом являются test‑filters — фильтры, через которые проходят тесты.
* признана экстремистской и запрещена на территории РФ.

Таких фильтров три:
Builds. Проверка влияния генерируемого теста на сборку приложения. Если генерируемый тест приводит к возникновению ошибок — он отбрасывается.
Passes. Проверка теста на стабильность. Тест всегда дает один и тот же результат — пропускаем его дальше.
Improve coverage. Проверка на увеличение покрытия кода. Если генерация новых тестов не приводит к увеличению покрытия — такие результаты следует отсеять.

TestGen-LLM зарекомендовала себя — около 25 % сгенерированных тестов проходят все фильтры.

Благодаря такому подходу компания смогла получить 73 % полезных тестов (из общего числа сгенерированных и прошедших фильтры) и увеличить Code Coverage на 11,5 %.
Это хорошие результаты для больших проектов, поэтому подход можно применять для решения наших задач.
Исследование рынка
Перед тем как приступать к написанию кода, мы поискали уже существующие похожие решения на рынке.
Одним из таких является плагин JetBrains AI, который интегрируется прямо в IntelliJ IDEA и позволяет работать с LLM, поддерживая множество языков программирования. Разработчики обещают удобство и простоту использования, но для наших задач у этого инструмента есть несколько существенных минусов:
запросы обрабатываются сторонним сервисом, который для нас — «чёрный ящик»;
это закрытое решение, и мы не можем контролировать, как именно обрабатываются данные или адаптировать процесс под свои нужды;
плагин платный, что ограничивает масштабирование.
В итоге нам нужно более гибкое, безопасное и подстраиваемое под наши требования решение.
ProxyAI
Второй вариант — ProxyAI.
ProxyAI (бывш. Code GPT) — чат с LLM внутри IDEA, который имеет открытый исходный код и позволяет локально запускать LLM на компьютере пользователя. Для работы с ProxyAI достаточно установить модель и запустить локальный сервер. Саму модель можно гибко настраивать, изменяя фильтры креативности (temperature, top‑p, top‑k и другие).

Приэтом запросы к LLM локальные и никуда не передаются, а при необходимости можно обращаться к облачным моделям по API.
Вместе с тем, вариант с ProxyAI тоже не лишен недостатков. Так:
плагин надо дорабатывать под нужды проекта;
надо делать слишком много ручных действий для генерации — например, вручную запускать плагин и писать промт на генерацию тестов;
подобное решение трудно интегрировать в CI.
Поэтому ProxyAI — не панацея. Его использование всё ещё требует значительных усилий для интеграции и автоматизации процесса генерации юнит‑тестов.
Самостоятельная реализация
Остаётся ещё один вариант — собственная реализация скрипта, который будет отправлять запросы к модели, обрабатывать сгенерированные тесты и автоматически интегрировать их в проект.
Так, мы получаем:
локальные запросы к собственной инфраструктуре и API, без передачи данных сторонним сервисам;
полный контроль над процессом генерации — ведь это наш код, и мы можем дорабатывать алгоритмы и логику столько, сколько потребуется;
полную автоматизацию: достаточно запустить один скрипт, чтобы получить готовые тесты;
бесплатное использование с учётом уже имеющихся инструментов и ресурсов — оплата только временем команды.
Главный недостаток — такую систему нужно строить самостоятельно, и это требует усилий и времени.
Однако мы были готовы к этому вызову и решили идти именно этим путём. И уже практически приступили к написанию кода, но…
Qodo-Cover
Уже после того, как мы смирились с необходимостью все писать вручную, в марте прошлого года появилась beta‑версия Python‑библиотеки Qodo‑Cover, которую можно использовать для генерации тестов и подсчета покрытия.

В чём преимущества Qodo‑Cover:
у него открытая кодовая база;
решение полностью автоматизировано;
запросы к LLM не передаются на сторону;
решение предоставляет фасад для генерации тестов с LLM — всё работает «из коробки».
Но решение нужно несколько дорабатывать под проект.
Доработка Qodo-Cover под наш проект
На первый взгляд кажется, что можно обойтись Qodo‑Cover в чистом виде.
Так, у Qodo‑Cover много флагов под разные задачи (некоторые из флагов обязательные — без них генерация не запустится):
--model
— выбор модели;--api-base
— выбор API для запросов к модели;--source-file-path
— установка source‑файла, для которого будут генерироваться юнит‑тесты;--test-file-path
— установка test файла, в нём подсчитываетсяcode-coverage
и вставляются сгенерированные тесты дляsource-file
;--code-coverage-report-path
— путь записиcoverage-report
;--test-command
— команда для запуска тестов в вашем проекте.
Также имеются вспомогательные флаги, через которые можно тонко настраивать расчёт покрытия и генерацию тестов:
--coverage-type
— выбор инструмента для расчёта покрытия;--desired-coverage
— назначение желаемого покрытия в процентах;--additional-instructions
— дополнительный промпт, который конкатенируется с системным промптом, и другие.
cover-agent \
--model "openai/qwen-3-32b" \
--api-base "https://api.copilot.vk.team/v1" \
--source-file-path "projects/utils/calcValue.ts" \
--test-file-path "projects/utils/calcValue.test.ts" \
--code-coverage-report-path "coverage/cobertura-coverage.xml" \
--test-command "jest --collect-coverage --coverageReporters=cobertura -c ./jest.config.ts $TEST_FILE_PATH" \
--coverage-type "cobertura" \
--desired-coverage 80 \
--max-iterations 3 \
--additional-instructions "Generate high-quality unit tests"
Для установки всего необходимого достаточно одного скрипта.
На практике довольно быстро стало понятно, что использовать Qodo‑Cover в виде Python‑библиотеки «как есть» — далеко не самое удобное решение. Настройка окружения оказалась громоздкой, а ещё нам нужно было больше гибкости: хотелось легко добавлять собственные обработки и адаптировать процесс генерации тестов под наш стек и сценарии. Кроме того, отсутствие централизованного способа обновления библиотеки добавляло неудобств, особенно в команде.
Мы пришли к очевидному решению — обернуть Qodo‑Cover в отдельный проект @ok/ut‑gen и поставлять его как npm‑пакет. Это дало нам сразу несколько плюсов: удобную установку, гибкое управление зависимостями и возможность добавления слоев с дополнительной логикой.
Сложности с окружением мы решили довольно просто. Разработчики Qodo‑Cover вместе с каждым релизом выкладывают готовые бинарные файлы под macOS, Windows и Linux. Ими мы и воспользовались.
Бинарники мы поставляем как зависимость в виде отдельного пакета — по одному для каждой ОС. Это удобно позволяет сделать npm. Операционную систему достаточно указать в поле os
внутри package.json. Затем остается прописать их в optionalDependencies
основного проекта, и npm автоматически устанавливает нужную версию в зависимости от операционной системы пользователя.

Это позволило нам полностью избавиться от необходимости ручной настройки среды — всё работает «из коробки».
Сценарий запуска оказался тоже простым: код на JavaScript формирует нужные параметры, запускает соответствующий бинарник Qodo‑Cover, и параллельно обрабатывает результат генерации.
Теперь подробнее о «подводных камнях».
«Подводные камни» при работе с Qodo-Cover
Пустой тест-файл
Первое, с чем мы столкнулись — запуски Qodo‑Cover для новых модулей начали падать с ошибкой.
При разборе выяснилось, что Qodo‑Cover перед генерацией ведет подсчет покрытия, то есть тест‑файл не должен быть пустым. Для корректной работы мы начали добавлять тест‑заглушку в файл до запуска генерации и удалять его в конце.

Недостающие импорты
Следующая проблема, с которой мы столкнулись, была предсказуемая, но всё же неприятная: независимо от того, как формулируется промпт, модели часто забывают добавлять импорты в сгенерированные тесты. В результате даже корректно написанные тесты просто не запускаются из‑за ошибок.
Чтобы не терять работоспособные тесты, мы решили взять этот этап под контроль и автоматически добавлять все необходимые импорты в тестовые файлы. Самый надёжный способ сделать это — воспользоваться абстрактными синтаксическими деревьями (AST, Abstract Syntax Tree).
Реализация оказалась довольно прямолинейной:
Мы получаем AST исходного и тестового файлов.
Из исходного файла извлекаем экспортируемые сущности, из тестового — уже присутствующие импорты.
Сравниваем их, определяем, какие импорты отсутствуют.
Формируем нужные import‑выражения, аккуратно формируя пути до исходного модуля, и вставляем их в начало тестового файла.

Такой подход гарантирует, что каждый тест будет иметь всё необходимое для запуска, независимо от того, насколько внимательна была модель при генерации.
Дубликаты импортов
Обратная ситуация тоже не редкость — модели иногда добавляют дублирующие импорты, что приводит к ошибкам.
Чтобы этого избежать, мы встроили дополнительную проверку. На каждой итерации генерации, вместе с запуском тестов, мы вызываем функцию, которая обходит тестовые файлы и автоматически удаляет дублирующиеся import‑выражения.

Это позволило избежать сбоев в пайплайне и сделать процесс более стабильным.
Про поиск используемых модулей
Одним из очевидных направлений для улучшения качества генерации стал сбор дополнительного контекста. Например, если модуль содержит импорты, их необходимо учитывать — без этого модель просто не сможет сгенерировать корректный и связанный с остальным проектом код.
К счастью, Qodo‑Cover поддерживает передачу списка дополнительных файлов, которые включаются в промпт. Это позволяет расширить контекст и улучшить точность генерации.
Оставалось только понять, как собрать этот список. Здесь снова выручили AST. Мы реализовали рекурсивный обход импортов:
начинаем с основного файла;
получаем его AST и проходимся по дереву, чтобы собрать все выражения импортов;
резолвим пути модулей для каждого импорта, включая проектные алиасы, относительные пути и файлы из
node_modules
;повторяем этот процесс для каждого найденного модуля.

В результате получаем полный список зависимостей, который передаём в Qodo‑Cover как часть входного контекста. Это позволяет LLM лучше понимать структуру проекта и генерировать более корректные и «вписанные» в кодовую базу тесты.
Стратегии улучшения промтов
Есть несколько стратегий, позволяющих улучшить промты.
Первая из них — Chain‑of‑Thoughts. Это техника, при которой модель просят объяснять ход своих рассуждений шаг за шагом. Благодаря этому модель дообучается и начинает генерировать более качественный код. Обычно стратегию применяют на уровне системного промта — его можно модифицировать и подстраивать под свои нужды.
Наряду с Chain‑of‑Thoughts есть и shot‑техники — Zero‑Shot и Few‑Shot Prompting.
Zero‑Shot подразумевает, что модель выполняет задачу без примеров.
Few‑Shot подразумевает, что перед основной задачей даются несколько примеров, чтобы модель лучше поняла, что от неё требуется.

Мы протестировали обе shot‑техники и получили довольно ощутимые результаты.
Так:
При использовании Zero‑Shot сгенерированные тесты увеличили покрытие до 65 % в рамках одного тест‑файла.
С применением few‑shot техники результат оказался лучше — в некоторых случаях покрытие достигло 100 %. Разница значительная.

В результате этих и некоторых других более мелких доработок у нас получилась собственная библиотека для генерации тестов на основе Qodo‑Cover.
Чтобы начать использовать ее в проекте, нужно всего лишь установить пакет и задать пару настроек в конфиге.

После этого необходимо вызвать команду npx generate-tests
. Тесты сохраняются в соответствующий файл, а разработчику остается только провалидировать результат генерации и закоммитить изменения.

Примеры тестов
Наконец, давайте посмотрим, каких результатов удалось добиться на практике. Вот реализация классической функции debounce
из проекта:
export interface IDebounceReturnFunction {
(...args: unknown[]): void;
cancel(): void;
}
export function debounce(ms: number, f: Function, ctx: object | null = null): IDebounceReturnFunction {
let timer: null | number = null;
const returnFunction: IDebounceReturnFunction = (...args: unknown[]) => {
const onComplete = () => {
f.apply(ctx, args);
timer = null;
};
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(onComplete, ms);
}
returnFunction.cancel = () => {
if (timer) {
clearTimeout(timer);
}
};
return returnFunction;
}
И с ней утилита справляется неплохо:
import { debounce } from './debounce';
describe('debounce', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('resets timer on subsequent calls', () => {
const mockFn = jest.fn();
const debounced = debounce(100, mockFn);
debounced();
debounced();
jest.advanceTimersByTime(100);
expect(mockFn).toHaveBeenCalledTimes(1);
});
it('cancels pending debounced function execution', () => {
const mockFn = jest.fn();
const debounced = debounce(100, mockFn);
debounced();
debounced.cancel();
jest.runAllTimers();
expect(mockFn).not.toHaveBeenCalled();
});
it('executes debounced function after delay with arguments', () => {
const mockFn = jest.fn();
const debounced = debounce(100, mockFn);
debounced('arg1', 'arg2');
jest.advanceTimersByTime(100);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
});
});
Сгенерированные тесты:
корректно используют
jest.useFakeTimers()
иjest.advanceTimersByTime()
для имитации задержек;проверяют важные сценарии: сброс таймера при повторных вызовах, отмену выполнения и передачу аргументов;
охватывают ключевое поведение функции и оформлены в понятной, читаемой форме.
Еще один пример — функция cleanDuplicateImports
, которая используется в проекте @ok/ut‑gen. Её задача — очищать модули от дублирующихся импортов после генерации тестов. Здесь особенность в работе с файловой системой — чтение и запись файлов, манипуляции с содержимым кода:
/**
* Очищает код в модуле от дублирующихся импортов
*/
export default function cleanDuplicateImports(sourceFilePath: string, testFilePath: string): void {
const code = fs.readFileSync(testFilePath, 'utf8');
fs.writeFileSync(testFilePath, fixImports(code, getImportSource(sourceFilePath)), 'utf8');
}
Несмотря на это, модель успешно сгенерировала серию тестов, охватывающих разные сценарии — от простых файлов без импортов до случаев с множественными дубликатами и пустыми строками между ними:
import { beforeAll, expect, test } from 'vitest';
import cleanDuplicateImports from '../src/post-process/clean-duplicate-imports.ts';
import fs from 'node:fs';
beforeAll(() => {
if (!fs.existsSync('specs/fixtures')) {
fs.mkdirSync('specs/fixtures');
}
});
test('test_no_imports', () => {
const filePath = 'specs/fixtures/test_no_imports.ts';
fs.writeFileSync(filePath, "console.log('No imports here');");
cleanDuplicateImports('some-file.ts', filePath);
const result = fs.readFileSync(filePath, 'utf8');
expect(result).toBe("console.log('No imports here');");
fs.unlinkSync(filePath);
});
test('test_no_duplicate_imports', () => {
const filePath = 'specs/fixtures/test_no_duplicates.ts';
fs.writeFileSync(filePath, "import { foo } from './test_no_duplicates';\nconsole.log(foo);");
cleanDuplicateImports('test_no_duplicates.ts', filePath);
const result = fs.readFileSync(filePath, 'utf8');
expect(result).toBe("import { foo } from './test_no_duplicates';\n\nconsole.log(foo);");
fs.unlinkSync(filePath);
});
test('test_multiple_duplicates_with_empty_lines', () => {
const filePath = 'specs/fixtures/test_multiple_duplicates.ts';
fs.writeFileSync(filePath, "import { foo } from './test_multiple_duplicates';\n\nimport { bar } from './test_multiple_duplicates';\n\nimport { foo, bar } from './test_multiple_duplicates';\n\nconsole.log(foo, bar);");
cleanDuplicateImports('test_multiple_duplicates.ts', filePath);
const result = fs.readFileSync(filePath, 'utf8');
expect(result).toBe("import { foo, bar } from './test_multiple_duplicates';\n\nconsole.log(foo, bar);");
fs.unlinkSync(filePath);
});
test('test_duplicate_imports', () => {
const filePath = 'specs/fixtures/test_duplicates.ts';
fs.writeFileSync(filePath, "import { foo } from './test_duplicates';\nimport { bar } from './test_duplicates';\nimport { foo, bar } from './test_duplicates';\nconsole.log(foo, bar);");
cleanDuplicateImports('test_duplicates.ts', filePath);
const result = fs.readFileSync(filePath, 'utf8');
expect(result).toBe("import { foo, bar } from './test_duplicates';\n\nconsole.log(foo, bar);");
fs.unlinkSync(filePath);
});
Сгенерированные тесты:
создают фикстуры на лету;
покрывают несколько крайних кейсов, включая корректный код, повторяющиеся импорты и сложные комбинации.
Это подтверждает, что инструмент способен не только «угадывать» простейшие вызовы функций, но и генерировать стабильные и осмысленные тесты даже там, где затронуты побочные эффекты — таймеры, файловая система, структура кода.
Как завести генерацию юнит-тестов?
Итак, если у вас есть желание быстро завести генерацию юнит‑тестов на своем проекте, достаточно минимального набора: Qodo‑Cover и доступа к любой LLM через API.
Для лучшей работы Qodo‑Cover и стабильной генерации потребуется выполнить еще несколько шагов:
подобрать оптимальные параметры под проект и настроить все доступные флаги, включая
--additional-instructions
;помогать в сборе контекста и передавать его модели — вышеупомянутый обход AST со сбором импортов;
править проблемы, возникающие при генерации в вашем проекте — тест‑заглушка для новых тест файлов и другие.
Что в итоге
В результате мы получили полноценный инструмент, который позволяет автоматически генерировать юнит‑тесты с минимальным участием разработчиков. Это дало ощутимый эффект: нам удалось покрыть тестами ключевые модули проекта и увеличить общее покрытие на 20%.
Но главное — мы значительно сократили время, необходимое на написание юнит‑тестов, и избавили разработчиков от рутины.
А вы пишете юнит‑тесты? Верите, что через пару лет LLM смогут писать не только тесты, но и полноценные модули ваших проектов?
Делитесь мнением в комментариях — будет интересно обсудить.
evgeniy_kudinov
Спасибо, вполне практически полезная информация про Qodo‑Cover. Пока вручную пишу запросы для генерации тестов, результат разный, но ускоряет рутину это точно. Хотя проект Qodo‑Cover, написали, уже не поддерживается, но надо поиграться с ним и проверить концепцию.