| Пишем тесты с Claude Code, часть 1: первичные результаты |
| 12.08.2026 00:00 |
|
В недавней статье я писал о том, как использовал Claude Code для анализа кода RestAssured.Net и выполнения рефакторинга, используя написанные вручную тесты в качестве страховочной сетки. В той статье я упомянул, что не хочу, чтобы Claude трогал сами тесты, и объяснил, почему. Тем не менее, мне было любопытно самому выяснить, на что способен Claude с точки зрения написания тестов и насколько оправдано доверие, которое всё больше людей возлагают на тесты, созданные LLM. В этой статье я поделюсь первыми шагами в этом направлении, а также своими мыслями и ходом рассуждений на этом пути. Вы увидите, как я создаю начальный набор тестов для небольшого API на Spring Boot, который я написал для использования в своих воркшопах, и как я оценивал результат. В следующей статье я покажу, как улучшил набор тестов на основе своих наблюдений, снова используя Claude Code. Отправная точка В качестве отправной точки я создал новый репозиторий с кодом API, для которого хотел, чтобы Claude написал тесты. Разумеется, я удалил существующие тесты, а также README и конфигурацию пайплайна сборки GitHub Actions, поскольку хотел, чтобы Claude писал тесты, опираясь только на продуктовый код, без подсказок из других артефактов репозитория. Я оставил зависимости, используемые для написания и запуска тестов, в данном случае REST Assured и JUnit. После установки и инициализации Claude я попросил его сгенерировать тесты с помощью следующего промпта: «Добавь acceptance-тесты для эндпоинтов, предоставляемых AccountController, в этот проект. Покрой всю логику в классе AccountService. Используй REST Assured для взаимодействия с API. Используй JUnit 5 в качестве тестового раннера. Обе библиотеки уже подключены в проекте, смотри в pom.xml. В тестах проверяй статус-коды и релевантные элементы тела ответа. Вынеси общие свойства запроса в RequestSpecification». Спустя некоторое время Claude добавил в проект новый файл с тестами, содержащий 23 теста, и все они успешно проходят. Эти тесты можно посмотреть здесь. Код в этом файле — это сырой результат выполнения указанного промпта, я ничего не менял. Claude понадобилась всего пара минут, чтобы написать эти тесты, что, безусловно, значительно быстрее, чем я сделал бы сам. Но насколько они на самом деле хороши? Первый взгляд на тестыСначала посмотрим на стиль кода. Я вижу, как некоторые утверждают, что качество кода уже не так важно, когда большую часть кода будет писать ИИ, но я с этим не согласен, особенно когда речь идёт о тестах. Тесты — это документация ожидаемого поведения кода, и возможность читать эту документацию без лишних усилий по-прежнему крайне важна. Так вот, легко ли читать сгенерированный код? Есть хук @BeforeEach, который создаёт RequestSpecification (объект в REST Assured, содержащий общие свойства HTTP-запроса). Есть вспомогательный метод для создания нового аккаунта с передачей AccountType и предопределённого баланса. Есть упомянутые 23 теста, которые, по крайней мере на первый взгляд, проверяют полезные вещи. Чего Claude не сделал, скорее всего потому, что я об этом явно не попросил, — это не добавил уровень абстракции, чтобы сделать код более читаемым, аналогично подходу, описанному здесь. Посмотрим, как Claude справится с этим, в следующей статье, а пока я хочу сосредоточиться на оценке качества первоначального результата. И должен сказать, что в целом для первой попытки результат неплохой. Да, есть пространство для улучшений, но я видел, как люди пишут тесты куда хуже. Конечно, это небольшой и простой API, но это не мешало людям (включая меня в прошлом) писать для него неопрятный тестовый код. Если посмотреть, что именно покрывают тесты, на первый взгляд кажется, что они затрагивают все эндпоинты, определённые в контроллере API, а также большинство, если не все, пути в бизнес-логике, реализованной в сервисном слое. Стоит отметить, что я смог довольно быстро и уверенно прийти к этим выводам только потому, что:
Если такого опыта и знаний нет, делать осмысленные выводы, просто глядя на результат работы Claude, будет сложнее. И это связано с серьёзным риском: сказать «выглядит нормально», не понимая, что именно одобряется и чему вы доверились, и в итоге получить полную дыр «страховочную сетку» тестов. Проверка сгенерированных тестов с помощью мутационного тестированияЧтобы лучше понять ценность сгенерированных тестов, посмотрим, способны ли они падать. Если нет, то 23 проходящих теста, написанных за две минуты, — это не более чем иллюзия продуктивности. Предпочитаемый мной способ проверить, может ли тест упасть, — использовать инструмент мутационного тестирования. В данном случае, поскольку речь идёт о Java, я использую PITest. Я настроил инструмент так, чтобы он мутировал весь код проекта и запускал все тесты, чтобы получить полное представление о качестве тестового набора. Обратите внимание, что в реальном проекте лучше начать с мутации части кода и запуска части тестов, чтобы получать обратную связь за разумное время. Примерно через минуту PITest сообщает, что исходный набор тестов достигает 95% покрытия строк. Выглядит впечатляюще, но на самом деле мало о чём говорит. Куда более важная метрика — количество «убитых» мутаций. PITest сообщает 91%, что тоже довольно хорошо. В абсолютных цифрах: из 55 мутаций 50 были обнаружены тестами. Вследствие этого у меня сразу возникло два вопроса:
Анализ выживших мутацийСначала посмотрим на мутации, которые «выжили», то есть изменения в коде API, которые не были обнаружены ни одним тестом.
Во-первых, в CustomizedResponseEntityExceptionHandler не покрыт путь HTTP 500, что и приводит к выжившей мутации. По задумке API возвращает HTTP 500, когда возникает Exception, не являющийся ResourceNotFoundException (HTTP 404) или BadRequestException (HTTP 400). Это явно полезный сценарий для тестирования.
Во-вторых, API возвращает HTTP 204 в ответ на GET-запрос к /accounts, если в базе данных нет аккаунтов. Этот путь также не покрыт тестами. Это тоже важный сценарий, поскольку он является частью ожидаемого поведения API.
Наконец, тесты не полностью покрывают некоторые граничные условия — как в логике запрета ухода в минус для сберегательного счёта, так и в расчёте процентов. Эти случаи тоже должны быть покрыты тестами. Эти результаты показывают, что мутационное тестирование — полезный способ понять, что покрыто тестами, а что нет. Неважно, написаны ли тесты вручную или сгенерированы LLM. Примечание: здесь сильно помогло то, что я хорошо понимаю, как работает мутационное тестирование и как интерпретировать его результаты. Что ещё важнее, эти результаты усиливают моё убеждение, что необходимо внимательно следить за тем, что генерирует LLM. Для меня важно писать тесты, которые проверяют значимые вещи и действительно способны обнаруживать изменения поведения продукта, и это не должно меняться при использовании ИИ. Если бы целью было просто получить тесты и, например, 90% покрытия строк считать «достаточным», на этом можно было бы остановиться. Но меня этот результат не устраивает по причинам, описанным выше. В следующей статье я передам эту обратную связь Claude и посмотрю, насколько он сможет улучшить существующий набор тестов. Также хочу попробовать включить мутационное тестирование в цикл генерации тестов, но это уже тема для другого раза. Пока можно сделать вывод, что при таком способе генерации тестов Claude даёт довольно хороший результат по покрытию, но пропускает некоторые важные сценарии. Выявление «балластных» тестовТеперь хочу понять, есть ли в наборе тестов лишние — такие, которые не вносят уникального вклада в покрытие. Для этого я попросил PITest сгенерировать XML-отчёт вместе с HTML, поскольку только в XML указано, какой тест «убил» какую мутацию. Анализ потребовал некоторой ручной работы: пришлось искать в XML-отчёте упоминания каждого теста. Это можно автоматизировать, но пока я сделал это вручную — тестов всего 23, а рисков «галлюцинаций» меньше. В результате оказалось, что 4 теста не фигурируют как убивающие мутации. Во всех случаях причина одна — тот же путь уже покрывается другими тестами. Например, один тест проверяет снятие средств с расчётного счёта и обновление баланса: @Test Другой тест делает то же самое, но для сберегательного счёта: @Test Поскольку логика обработки списания при достаточном балансе одинакова для всех типов счетов, одного теста достаточно. После удаления этих четырёх тестов и повторного запуска мутационного тестирования покрытие не изменилось, что подтверждает: это действительно «балласт». Зачем вообще это делать? Ведь эти тесты достались «бесплатно». Не совсем. Тесты требуют времени на выполнение и поддержку, а главное — времени на анализ их результатов. Чем меньше это время, тем лучше. Если можно безопасно удалить тесты без потери покрытия, это разумный шаг. ВыводыИтак, что можно сказать по итогам анализа? Я приятно удивлён качеством и покрытием первоначального набора тестов. 95% покрытия строк и 91% мутационного покрытия — хорошие показатели, особенно учитывая, что всё это получено за считанные минуты. Есть пространство для улучшения читаемости тестов, но это можно решить более точными промптами или использованием специальных возможностей Claude Code. Теперь о предостережениях. Несмотря на хорошее покрытие, Claude пропустил несколько важных сценариев. Возможно, это случайность, но такие результаты показывают, что нельзя слепо доверять результату. То же касается и количества тестов: 4 из 23 оказались лишними, то есть 17% набора. Да, выборка маленькая, но такие вещи игнорировать не стоит, если цель — эффективный тестовый набор. Наконец, есть много вещей, которые Claude не сделал, потому что я не попросил. Например, он не предложил добавить аутентификацию для банковского API. Есть ещё множество аспектов, которые стоит разобрать, и я, вероятно, напишу об этом позже. В следующей статье я опишу процесс улучшения набора тестов, снова используя Claude и мутационное тестирование. Код API и начальный набор тестов можно найти здесь. |