Как писать юнит-тесты с ИИ: практическое руководство

Как писать юнит-тесты с ИИ-агентами, которые ловят баги: характеризационные тесты, пробелы в покрытии, мутационное тестирование и разделение тестов и кода.

Solviera Teknoloji 10 мин чтения

Когда вы пишете юнит-тесты с ИИ, получить сами тесты легко. Попросите любого кодинг-агента написать «тесты для этого модуля», и через несколько минут у вас будет сорок зелёных тестов. Трудно получить тесты, которые падают, когда код неправильный. Агент, который прочитал функцию и затем написал проверки того, что она возвращает, по сути написал эту функцию второй раз.

В этом руководстве разберём, как получать полезные тесты от кодинг-агентов вроде Claude Code, Codex или Gemini CLI: что просить, как распознать тесты, которые ничего не доказывают, как измерять их покрытием и мутационным тестированием и как разделить работу так, чтобы агент, пишущий код, не мог тихо подогнать под него тесты.

Почему тесты от ИИ получаются плохими

Агенты отлично справляются с механической частью тестирования: фикстуры, фабрики, моки, параметризованные случаи, шаблон нового файла с тестами. Ошибаются они в том, что делает тест ценным, — в ожидаемом значении.

Когда агент пишет тест, реализация обычно уже лежит у него в контексте. Естественный ход — прочитать код, понять, что он возвращает, и проверить ровно это. Если в коде баг, тест теперь закрепляет баг. Он зелёный, строки покрыты, и он останется зелёным, даже если баг так и не исправят.

Типичные формы этой проблемы:

  • Зеркальная логика. Тест вычисляет ожидаемое значение той же формулой, что и код: expect(price(100, 0.2)).toBe(100 - 100 * 0.2). Если формула неверна, ошибаются оба.
  • Мок проверяемого объекта. То, что тестируется, замокано, и тест проверяет мок.
  • Проверка только вызова. expect(sendEmail).toHaveBeenCalled() без проверки, что и кому отправлено.
  • Снапшот на всё. Огромный снапшот, который никто не читает и который обновляют через -u при каждом падении.
  • Проверки, которые не могут упасть. expect(result).toBeDefined() для функции, которая всегда возвращает объект.
  • Правка теста ради зелёного. Код не проходит тест, и агент «чинит» ожидаемое значение в тесте вместо кода.

Ничего из этого не придумано ИИ. Люди пишут такие тесты тоже. Просто агенты делают это быстрее и большими партиями, поэтому нужен процесс, который такие тесты отлавливает.

Начинайте с поведения, а не с кода

Самое большое улучшение — сказать агенту, что код должен делать, и не давать реализации диктовать ожидаемые значения.

Слабый промпт:

Напиши юнит-тесты для src/billing/discount.ts.

Сильнее:

Напиши юнит-тесты для applyDiscount в src/billing/discount.ts. Правила: процентная скидка никогда не опускает сумму ниже нуля; скидки не суммируются, побеждает наибольшая; просроченный код игнорируется, это не ошибка; суммы округляются до 2 знаков, половина вверх. Ожидаемые значения выведи вручную из этих правил, не запуская и не читая реализацию. Если правило расходится с текущим кодом, всё равно напиши тест и скажи, какие тесты падают.

Изменились три вещи. Правила дали вы — или тикет, спецификация, документация. Ожидаемые значения — литералы, выведенные из правил. И падающий тест допустим: возможно, это настоящий баг, а именно для этого тесты и нужны.

Ещё несколько полезных привычек:

  • Сначала попросите список случаев. «Перечисли случаи, которые собираешься проверить, по одной строке, прежде чем писать код». Пропущенные граничные случаи (пустой ввод, юникод, часовые пояса, граница ровно в нуле) можно добавить до того, как появится код.
  • Назовите границы. Агенты хорошо находят граничные случаи, если указать измерение: «входы на каждой границе, чуть ниже и чуть выше неё».
  • Скажите, что не мокать. «Не мокай ничего внутри src/billing; мокай только HTTP-клиент». С избыточных моков начинается множество тавтологических тестов.
  • Одно поведение на тест. Тест с понятным именем вроде ignores_expired_codes проще ревьюить, чем один тест с двенадцатью проверками.

Юнит-, интеграционные и end-to-end тесты

Один и тот же агент может писать все три вида, но инструкции для них разные.

Юнит-тесты

Быстрые, изолированные, их много. Здесь агенты экономят больше всего времени. Укажите на свой стиль тестов («следуй tests/billing/tax.test.ts»), чтобы не получать каждый раз новую библиотеку хелперов. Пропишите команду тестов в CLAUDE.md или AGENTS.md, чтобы агент запускал тесты сам; подробнее — в статье как писать CLAUDE.md и AGENTS.md.

Интеграционные тесты

Они работают с настоящей базой данных, очередью или файловой системой. Агенты и здесь тянутся к мокам, и интеграционный тест снова превращается в юнит-тест. Говорите прямо: «используй тестовую базу из docker-compose.test.yml; не мокай слой репозитория». Попросите и очистку, чтобы тесты не зависели от того, что оставили другие.

End-to-end тесты

Для браузерных тестов агент может написать спецификации Playwright или Cypress по описанному вами пользовательскому сценарию. Слабое место — селекторы: просите селекторы по роли или подписи (getByRole('button', { name: 'Pay' })), а не CSS-пути, которые сломаются при следующем редизайне. О том, как дать агенту управлять настоящим браузером для проверки собственной работы, рассказано в статье тестирование в браузере с ИИ-агентом.

Характеризационные тесты для кода, который вы пока не понимаете

Иногда спецификации нет. Код старый, автор ушёл, и никто точно не знает, что он делает. Тогда цель меняется: нужны тесты, которые фиксируют, что код делает сегодня, включая баги, чтобы вы заметили, если это изменится.

Это характеризационные тесты, и здесь как раз правильно позволить агенту читать реализацию. Промпт меняется соответственно:

Напиши характеризационные тесты для legacy/invoice_export.py. Фиксируй текущее поведение, включая всё, что похоже на баг; ничего не исправляй. Покрой все ветки, которые найдёшь. Любое поведение, которое выглядит неправильным, отметь комментарием # SUSPICIOUS: с объяснением.

Пометки SUSPICIOUS станут вашим списком багов на потом. Снапшоты и approval-тесты здесь уместны, если они достаточно маленькие, чтобы их читать. Это первый шаг любого безопасного рефакторинга, о котором подробно рассказано в руководстве рефакторинг legacy-кода с ИИ.

Покрытие — чтобы найти пробелы, а не чтобы оценивать тесты

Покрытие — хорошая карта и плохая оценка. Используйте его, чтобы показать агенту, куда смотреть:

СтекКоманда
Vitestnpx vitest run --coverage
Jestnpx jest --coverage
pytest (с pytest-cov)pytest --cov=myapp --cov-report=term-missing
Gogo test -coverprofile=cover.out ./..., затем go tool cover -func=cover.out

Для Vitest нужен установленный провайдер покрытия, например @vitest/coverage-v8. Отчёт term-missing в pytest перечисляет точные номера непокрытых строк, и их удобно вставлять прямо в промпт.

Хороший цикл:

  1. Запустите покрытие и выберите важные модули: биллинг, авторизацию, миграции данных, а не сгенерированный код.
  2. Дайте агенту непокрытые строки и спросите, какое поведение за ними стоит.
  3. Пусть он напишет тесты на это поведение с ожидаемыми значениями из правил.
  4. Перезапустите покрытие и убедитесь, что строки теперь выполняются.

Чего делать не стоит — говорить «доведи покрытие до 90%». Агент, получивший число, достигнет числа, и самый дешёвый способ — тесты, которые выполняют строки, почти ничего не проверяя.

Мутационное тестирование: проверяют ли тесты хоть что-то?

Мутационное тестирование отвечает на вопрос, на который покрытие ответить не может. Инструмент вносит в код мелкие изменения — меняет > на >=, + на - или возвращает пустое значение — и прогоняет тесты на каждом изменении. Если тесты всё равно проходят, мутант «выжил», и вы нашли место, где тесты на самом деле не проверяют поведение.

Распространённые инструменты:

  • JavaScript и TypeScript: StrykerJS. Настройка — npm init stryker@latest, запуск — npx stryker run.
  • Python: mutmut. Запустите mutmut run, затем mutmut browse, чтобы пройтись по выжившим мутантам. Свежим версиям нужна поддержка fork, поэтому на Windows он работает под WSL.
  • Java: PIT, например mvn test-compile org.pitest:pitest-maven:mutationCoverage.
  • Rust: cargo-mutants, устанавливается через cargo install cargo-mutants и запускается командой cargo mutants.

На большой кодовой базе мутационные прогоны медленные, поэтому ограничьте их модулем, над которым работаете. Затем отдайте выживших агенту:

В discount.ts выжили эти мутанты: (список). Для каждого напиши тест, который падает на мутанте и проходит на оригинале. discount.ts не меняй.

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

Один агент пишет тесты, другой — код

Самый надёжный способ не дать агенту подогнать тесты — сделать так, чтобы агент, пишущий код, не мог их трогать.

Схема простая:

  1. Агент-тестировщик. Получает спецификацию и пишет тесты в tests/ (или туда, где у вас лежат тесты). Реализацию он не видит и не пишет. Сначала его тесты должны падать, ведь фичи ещё нет.
  2. Агент-реализатор. Получает ту же спецификацию и падающие тесты и пишет код, пока они не пройдут. Ничего в tests/ ему править нельзя.
  3. Разногласия возвращаются обратно. Если реализатор считает тест неверным, он спрашивает агента-тестировщика или вас, а не правит тест.

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

Двум агентам на одной фиче нужна изоляция, чтобы они не мешали друг другу. Обычный ответ — отдельный git worktree на агента; см. git worktree для параллельных агентов. При таком разделении можно оставить обоих и в одном worktree, ведь они пишут в разные папки.

Ревью тестов, написанных ИИ

Читайте тесты не менее внимательно, чем код. Короткий чек-лист:

  • Ожидаемые значения взяты из правил? Ищите литералы, а не формулы, скопированные из кода.
  • Упадёт ли тест, если код неправильный? Возьмите один тест и сломайте код вручную. Если тест остался зелёным, это декорация.
  • Не замокано ли что-то важное? Моки на границе системы — нормально; моки проверяемого кода — нет.
  • Изменился ли какой-то существующий тест? В ветке фичи изменённые ожидаемые значения в старых тестах заслуживают вопроса. В диффе это сразу видно через git diff --stat -- tests/.
  • Читаются ли имена? По имени упавшего теста должно быть понятно, что сломалось.
  • Не флаки ли он? Следите за реальным временем, случайными данными, сетевыми вызовами и порядком тестов. Попросите агента прогнать новые тесты несколько раз.

Здесь помогает и автоматическое ревью кода. Вторая модель, читающая дифф, часто замечает тест, который ничего не проверяет. Как это работает, рассказано в статье ИИ-ревью кода.

Частые ошибки

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

Как это сделать в AgentVera

AgentVera запускает CLI кодинг-агентов бок о бок в каждом проекте, поэтому схема «тестировщик и реализатор» настраивается за пару минут.

  • Два агента, одна фича. Откройте в одном проекте агента-тестировщика и агента-реализатора — на одном CLI или на разных, например Claude Code для тестов и Codex для кода. Если хотите вынести работу из основной папки, поместите их в отдельный worktree через панель worktree.
  • Владение файлами не пускает реализатора в тесты. В диалоге агента-тестировщика добавьте tests/** в его зоны владения. Пока он работает, другие агенты не могут править эти файлы. Для агентов Claude Code правка отклоняется ещё до выполнения хуком PreToolUse; Codex и другие CLI получают правило в своих инструкциях. Отклонённые попытки видны на таймлайне команды.
  • Обмен сообщениями между агентами для разногласий. Если реализатор считает тест неверным, он может написать агенту-тестировщику напрямую и дождаться ответа, а не править тест.
  • Verify проверяет результат. Опишите в сообщении, что значит «готово», например «все тесты в tests/billing проходят, ни один файл тестов не изменён». Verify превращает это в критерии, и когда ход агента заканчивается, отдельный верификатор запускает тесты и отмечает каждый критерий с доказательством. Верификатор может читать репозиторий и запускать команды тестов, но не может править файлы.
  • Ревью кода читает тесты. Автоматическое ревью проверяет изменения каждого хода выбранной вами моделью Claude и перечисляет замечания с уровнем серьёзности и file:line — хороший второй взгляд на слабые проверки.
  • Чекпоинты для неудачных ходов. Каждый ход сохраняется снимком, так что если агент переписал половину набора тестов не туда, вы откатываете этот ход и пробуете снова. См. как отменить изменения Claude Code.

Подводим итоги

ИИ делает написание тестов дешёвым, а значит, работа смещается от набора тестов к их оценке. Давайте агенту поведение, а не код, не позволяйте реализации диктовать ожидаемые значения, используйте покрытие как карту, а мутационное тестирование как оценку, и не позволяйте агенту, который пишет код, решать, что значит «прошло». Тогда скорость будет настоящей: набор тестов быстро растёт и всё равно падает, когда что-то ломается.

Читайте также:

Чтобы попробовать AgentVera, начните с главной страницы.

Вопросы

Может ли ИИ писать хорошие юнит-тесты?

Да, если им управлять. Агенты хорошо справляются с подготовкой окружения, фикстурами, списками граничных случаев и шаблонным кодом. Слабое место — ожидаемые значения, выведенные из проверяемого кода: тогда тест просто повторяет реализацию. Дайте агенту ожидаемое поведение и проверьте тесты мутационным тестированием.

Что такое тавтологический тест?

Тест, который проходит по построению: он вычисляет ожидаемое значение так же, как код, мокает именно то, что должен проверять, или проверяет только факт вызова функции. Он остаётся зелёным, даже когда логика неверна.

Стоит ли добиваться 100% покрытия для тестов от ИИ?

Нет. Покрытие говорит, какие строки выполнились, а не что что-то проверялось. Используйте его, чтобы найти непротестированные места, а качество тестов оценивайте мутационным тестированием или внимательным ревью.

Должен ли один агент писать и тесты, и код?

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

Какие агенты можно использовать для этого в AgentVera?

Любой CLI-агент, который запускает AgentVera, например Claude Code, Codex или Gemini CLI. Для агентов Claude Code владение файлами блокирует правку заранее; Codex и другие CLI получают правила владения в своих инструкциях.

Соберите своих агентов за одним столом.

Скачайте AgentVera бесплатно; ваши установленные CLI уже готовы к работе.

Другие статьи