AI-driven TDD — используем Code-LLM на максимум
С момента своего появления и по сей день подход Test-Driven Development (TDD) вызывает оживленные дискуссии в сообществе разработчиков, и до сих пор нет единого мнения о ее эффективности.
Но что будет, если совместить TDD и AI-генерацию кода? В статье я покажу:
Как соединить TDD и AI;
Как AI-driven TDD улучшает процесс разработки;
Как TDD влияет на качество сгенерированного AI кода.
А кроме того, попытаюсь немного поразмышлять относительно того, как будет развиваться область взаимодействия человека и AI в кодогенерации в ближайшие годы.
Кратко о TDD
Разработка через тестирование (Test-Driven Development, TDD) — это методология программирования, при которой тесты пишутся до написания кода. Процесс строится на коротких итерациях: сначала создается тест, затем реализуется минимальный код для его прохождения, после чего код рефакторится. Утверждается, что такой подход помогает создавать надежное и поддерживаемое программное обеспечение, снижая вероятность ошибок и улучшая архитектуру кода.
Цикл TDD
Рабочий процесс в TDD выглядит примерно следующим образом:
Написать тест;
Проверить, что тест не проходит («красная» стадия);
Написать реализацию, проходящую тест («зеленая» стадия);
Выполнить рефакторинг, чтобы все выглядело прилично и чтобы в нем могли разобраться другие люди (как минимум, чтобы следующие поколения поддерживающих ваш код не хотели вас убить);
Придумать новый тест, который не проходит; И так до тех пор, пока в голову не будет приходить ни одного тестового кейса, на котором код не работает, либо до того момента, когда добавление новых тестовых примеров уже нецелесообразно.
Про TDD написано огромное количество книг и статей. Для примера приведу 2 статьи и две книги:
TDD+AI
Но причем здесь AI?
Используя AI мы можем заниматься только «красной» стадией — написанием тестов, а «зеленую» (реализацию) и «желтую» (рефакторинг) почти полностью делегировать LLM.
Такой подход позволяет:
во-первых, повысить эффективность рабочего процесса — тратить меньше времени и сил на написание кода;
во-вторых, повысить надежность сгенерированного кода и уверенность в его работе, а также значительно снизить время отладки;
в-третьих, повышает качество сгенерированного кода, о чем будет далее.
TDD решает две большие проблемы, возникающих в процессе работы с AI code-ассистентами.
Первая — объяснить задачу LLM так, чтобы оно поняло, что требуется реализовать. Если написан интерфейс нового функционала и тесты к нему — лучшего и более конкретного описания просто не придумать.
Вторая — если код написала LLM, а не ты сам, то как быть уверенным в том, что он вообще рабочий? Или что в нем нет ошибок? Код из LLM с виду почти всегда выглядит неплохо. Oh, wait, так мы же уже написали тесты! Если код неправильный, то ошибку можно отправить на исправление назад LLM; и так до тех пор, пока не будет получен рабочий код, проходящий все написанные тесты.
Цикл AI-driven TDD
С AI-driven TDD написание кода вообще сводится к написанию тестов, но если взглянуть на это более глобально, то к описанию того, что конкретно, должен делать код, а не как. За то, как это делать, отвечает LLM. На разработчика ложится задача продумать максимально возможное количество тестов.
На момент написания статьи лично на моем опыте LLM способны реализовать большую часть функционала, который мне требуется. Возможно, не с первого раза, но после цикла исправлений ошибок, почти всегда.
LLM способны не только решать простые примеры, но и достаточно сложные. Это подтверждается последними бенчмарками. Но и в целом, они пишут код не хуже среднего программиста.
У GPT-o1 достигает точности 76% на бенчмарке LiveCodeBench, который составлен из задач разной сложности с LeetCode, AtCoder и Codeforces. Многие ли программисты сами способны набрать такое качество? Часть, безусловно, да. Но тем не менее, это показывает, что код писать LLM умеют неплохо. И почему бы это не использовать?
У остальных моделей показатели конечно похуже, но скорее всего через год-два более массовые модели (вроде текущих GPT-4o и Claude Sonnet-3.5), будут достигать подобной точности.
Последняя стадия — рефакторинг. На самом деле LLM весьма недурно умеют структурировать и рефакторить готовый код. Наверное поэтому почти во всех code-ассистентах есть кнопка «refactor». В большинстве случаев код после рефакторинга действительно неплохо структурирован и не нуждается в дополнительных изменениях. Но тем не менее иногда полезно добавить комментариев, переименовать переменные и т.п.
Как это выглядит в жизни
Для работы подходит любой code assistant, в котором есть чат и возможность нажать кнопку «insert», чтобы вставить предложенный код. Я пробовал на Codeium и Cursor AI. Без этого — слишком неудобно, все остальное — неважно. Я использую Cursor.
Шаг 1. Задаем промпт. В первую очередь открываем чат с LLM и вставляем инструкцию. Например, такую:
You are a code assistant that follows TDD principles to generate minimal
implementations:
1. Implement only the **minimal** code necessary to pass the provided tests.
2. Do not add extra logic, optimizations, or error handling unless
explicitly tested.
3. Use the simplest correct approach to satisfy all test cases.
4. If a test expects an exception, implement the exact behavior to raise it.
5. Do not anticipate future requirements—follow only what the tests define.
6. If new tests are added, incrementally refine the implementation.
7. Maintain Pythonic best practices while keeping the solution minimal.
8. If multiple valid implementations exist, choose the simplest one.
9. Your implementation should pass **all** provided tests before completion.
10. No additional functionality beyond what is explicitly required by the
tests.
Do **not** generate any code immediately. Instead, apply this TDD-driven
approach throughout our interaction, responding step by step as we refine
the implementation based on tests.
Главное, что требуется — упомянуть про минимальную реализацию, чтобы LLM не додумывала и не генерировала лишнее.
Шаг 2. Пишем тест. Как обычно — пишем тест.
Шаг 3. Заставляем машину работать. Выделяем свеженаписанный тест и добавляем два файла (или больше, если нужно) в контекст — файл с тестом и файл, где должна быть реализация, и пишем в чат слово Implement
.
Шаг 4. Оцениваем результат. После того, как LLM сгенерировала реализацию, запускаем тест и смотрим, что происходит. Если тест не проходит, то, как обычно при работе с LLM, копируем ошибку, пишем Fix
и отдаем traceback для исправления. И так до тех пор, пока тест не будет проходить.
Шаг 5. Рефакторинг. Если все проходит, то пишем слово Refactor if it is required
и (опционально) пробегаемся глазами по имплементации. Сама имплементация может подкинуть идеи, где слабые места и какие тесты еще можно добавить. А еще на этой стадии LLM, как правило, сама накидает достаточно неплохой docstring, что тоже удобно.
По поводу if it is required
— можно это не добавлять, но LLM рефакторнет любой код, который ей дать. С этой магической фразой избыточный рефакторинг выполняться не будет.
Шаг 6. GOTO Шаг 1. Пишем новый тест, и так далее.
В самом конце нужно сделать code review и подправить мелкие детали. Например, улучшить названия переменных, добавить какие-нибудь пояснения в docstring, и т.п.
Что машина НЕ должна делать
Машина не должна писать тесты. Тесты — зона ответственности программиста. Иначе, во-первых, невозможно гарантировать, что все работает как надо, а, во-вторых, не используется наиболее эффективный способ донесения требований до LLM. В классическом TDD тесты часто называют еще одним способом документировать код, и здесь тесты также выступают такой документацией, но в качестве некоего ТЗ машине — как список приемочных испытаний.
При этом, ничто не мешает в конце использовать всю ту же LLM проглядеть тесты на предмет того, что еще можно добавить. Но тесты должны быть написаны и отвалидированы разработчиком.
Кроме того, наличие ошибок в тестах опасно. LLM, следуя инструкциям реализует ровно то, что написано, даже если в тестах есть ошибка. И сгенерированный код естественно будет проходить их все. Поэтому с тестовыми примерами надо быть максимально внимательным.
Но работает ли это на самом деле?
В теории
Спасибо Канадским ученым из университета Ватерлоо, которые взяли и проверили, работает это или нет, а затем описали результаты в статье Test-Driven Development for Code Generation. И да, работает. Более того, работает достаточно круто — использование TDD заметно улучшает качество кодогенерации. Я покажу это на двух графиках из их работы.
Влияние TDD на точность решений
В статье они тестировали две модели: GPT-4 и Llama-3. Выводы совпадают для обоих моделей, но на Llama результаты вышли показательнее.
Здесь дана точность решения задач из датасетов MBPP и HumanEval. Оба датасета содержат в себе задачи на программирование на Python.
На графике:
Зеленым выделены задачи, которые модель решила по одному лишь описанию
Синим — с добавлением тестов
Оранжевым — решенные после подачи ошибки на вход LLM и последующего исправления
Красные — те, что так и остались нерешенными.
Так вот для MBPP добавление тестов позволяет решить дополнительно 33.6% задач, которые не были решены при первоначальной постановке без тестовых примеров.
Также авторы замерили влияние количества тестов на итоговый результат и показали, что увеличение количества тестов в целом улучшает результат.
Зависимость качества решений от количества тестов
На практике
Не сюрприз, что LLM пишет ход неплохо, но не идеально. И в большинстве случаев в сгенерированную реализацию приходится вносить изменения, чтобы сделать ее проще, добавить комментарии и т.п. Но даже при всей неидеальности, это заметно экономит когнитивные ресурсы или позволяет тупо меньше думать и меньше уставать. Вместо написания кода с нуля достаточно подождать, пока LLM напишет код, проходящий тесты, и провести ревью с внесением небольших правок. А сэкономленные силы потратить на что-нибудь еще. При этом, так как мы уделили достаточно внимание тестовым кейсам, можно спать спокойно даже при том, что код написали не мы, а бездушная машина.
К тому же, большой минус в том, что текущие инструменты не совсем приспособлены к такому подходу. Специально такой функционал нигде (насколько мне известно) не реализовыван. Но в Cursor, где есть возможность тыкать кнопку «apply» и легко добавлять и менять сгенерированный код, что уже достаточно удобно. Разве что приходится часто повторять промпт, чтобы AI вообще понимал, что от него требуется, а также после каждой итерации заново добавилять файлы в контекст. С другими инструментами ситуация может быть хуже. Но инструменты разработки с использованием AI тоже быстро прогрессируют.
Как итог, для меня лично AI-driven TDD позволяет не тратить ресурсы мозга на написание рутинных вещей и больше думать о надежности или о чем-нибудь другом, нежели о реализации. А также (судя по всему) бесплатно получить прирост качества генерации кода.
Программирование ближайшего будущего
Мне кажется, что с дальнейшим развитием кодогенерации и с тем, как LLM будут играть большую роль в написании кода, фокус разработки ПО будет двигаться от написания конкретных реализаций к формированию требований и проверок, гарантирующих правильность работы. То есть программирование будет становится максимально низкоуровневым requirements engineering-ом.
И хотя большинство code-ассистентов позволяют генерировать тесты, тесты — это именно та часть, которая должен остаться за людьми, а не отдаваться машинам. В общем и целом, программисты будут задавать требования и проверять, что все работает как надо, а машина будет писать реализацию.
Если представить себе, что LLM заменит всех программистов и отправит их работать курьерами, я пока не могу, то сценарий написания большей части кода — вполне. И в таком виде AI-driven TDD выглядит как один из лучших путей подобного рабочего процесса, так как со значительно большей увереностью позволяет полагаться на сгенерированный код, а заодно и увеличить эффективность взаимодействия с code-ассистентами.
В 2022 году, когда только была зарелизены GPT-3 и ChatGPT, на Reddit было обсуждение, можно ли ее использовать для написания кода с TDD? Комментариев там не много и большая часть из них касается спора вокруг самого TDD, нежели генерации кода, но мне понравился комментарий I don't see it happening as a general solution any time soon
. В том же 2022 году также появились несколько проектов на github, реализующих ту же идею: TDD-AI, TDD-GPT, но они так и остались на уровне proof-of-concept. Так вот, находясь в 2025 году, AI-driven TDD уже можно эффективно использовать прямо сейчас.
Выводы
AI-driven TDD позволяет:
снизить когнитивную нагрузку и меньше уставать при написании кода, таким образом позволяя уделять большее внимание более высокоуровневым вещам и надежности;
повысить надежность и уверенность в работе кода, сгенерированного LLM;
эффективнее взаимодействовать с code-ассистентами и получить прирост качества генерации кода.
LLM за последние 2 года сделали огромный прогресс и стали достаточно умны, чтобы взять на себу значительную часть разработческой рутины, так что грех этим не пользоваться. Но с большой силой также приходит и большая отвественность, так что вопрос тестирования стоит остро как никогда.