С момента своего появления и по сей день подход Test-Driven Development (TDD) вызывает оживленные дискуссии в сообществе разработчиков, и до сих пор нет единого мнения о ее эффективности.

Но что будет, если совместить TDD и AI-генерацию кода? В статье я покажу:

  • Как соединить TDD и AI;

  • Как AI-driven TDD улучшает процесс разработки;

  • Как TDD влияет на качество сгенерированного AI кода.

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

Кратко о TDD

Разработка через тестирование (Test-Driven Development, TDD) — это методология программирования, при которой тесты пишутся до написания кода. Процесс строится на коротких итерациях: сначала создается тест, затем реализуется минимальный код для его прохождения, после чего код рефакторится. Утверждается, что такой подход помогает создавать надежное и поддерживаемое программное обеспечение, снижая вероятность ошибок и улучшая архитектуру кода.

Цикл 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

С 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 на точность решений
Влияние TDD на точность решений

В статье они тестировали две модели: GPT-4 и Llama-3. Выводы совпадают для обоих моделей, но на Llama результаты вышли показательнее.

Здесь дана точность решения задач из датасетов MBPP и HumanEval. Оба датасета содержат в себе задачи на программирование на Python.

На графике:

  1. Зеленым выделены задачи, которые модель решила по одному лишь описанию

  2. Синим - с добавлением тестов

  3. Оранжевым - решенные после подачи ошибки на вход LLM и последующего исправления

  4. Красные - те, что так и остались нерешенными.

Так вот для 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 года сделали огромный прогресс и стали достаточно умны, чтобы взять на себу значительную часть разработческой рутины, так что грех этим не пользоваться. Но с большой силой также приходит и большая отвественность, так что вопрос тестирования стоит остро как никогда.

Комментарии (12)


  1. achekalin
    26.01.2025 10:32

    Т.е., заранее понимаю, что качество кода, сгенерированное нейросетью, не особо высоко, и более того, не обязательно решай даже поставленную перед сетью задачу, мы, вместо того, чтобы посадить туда толкового разработчика, либо как-то до обучить сеть, тупо обвешивает её кучей тестов, и долби все до последнего, пока тесты не пройдут?

    Смысл конечно в этом есть, однако же, мы понимаем, что нейросеть не решает напрямую задачу поставленную перед ней - она решает некую языковую задачку, которая, возможно, в итоге даст запускаемый код. В итоге, нам нужны очень хорошие тесты, которые покажут не просто наличие кода "на входе" проверки, и не просто подтвердят, что этот код запускается, ну и проверит все-все-все-все-все подробности работы этого кода. На глазок, кажется, что написание подобного теста не сильно проще, чем написание проверяемого кода.

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

    Зато экономия на разработчиках весомая, правда?

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


    1. slava0135
      26.01.2025 10:32

      (Юнит) тесты нужно писать в любом случае (уже жду споры о том что это не так и код должен работать на честном слове, хотя казалось бы 2025 год на дворе). В конце концов, если код написанный ИИ и код написанный человеком проходит один и тот же набор тестов, то какая разница, кто его писал? Да нужно сделать рефакторинг и т.п. И да, написание (хороших) тестов не проще чем реализация. Если не хватает уверенности - можно использовать фаззинг и PBT.

      Вообще, писать код и писать тесты по-хорошему должны разные люди, но это невозможно в TDD (если это не парное программирование).

      При написании кода условного самолета используют более строгие методы проверки качества, чем тестирование, вроде формальной верификации - только так можно проверить, что код соответствует спецификации (что не защищает от ошибок в самой спецификации).


    1. iavdonin Автор
      26.01.2025 10:32

      Я полностью согласен, что в тупую генерировать код критически важных систем нельзя. И пока в таких делах полагаться на LLM явно преждевременно.

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

      Но, как мне кажется, сейчас вопрос будет стоять несколько по другому. Код-ассистенты неизбежно будут использоваться в подобных компаниях в том числе. И с этим надо будет как-то жить. Я сейчас пробежался глазами по сайтам некоторых код-ассистентов и там нет производителей самолетов, но у GitHub Copilot в списке клиентов числится, например, BMW. Тоже ничего так.

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


  1. benjik
    26.01.2025 10:32

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

    Aider такое умеет. Делаешь в его консоли `/run <your-toolchain> tests` , он запускает тесты, и если они не прошли - предлагает отправить вывод в модель и пофиксить. Обычно справляется.

    Другое дело что он очень opinionated в плане ui/ux и не многим подойдёт.


    1. positroid
      26.01.2025 10:32

      Да и cursor так умеет, в YOLO реэиме и аппрувить запуск команд не нужно (можно белый список настроить)


  1. evgeniy_kudinov
    26.01.2025 10:32

    Тоже про такое думаю (процесс LLM+TDD) для написания тестов, так как это полезное практическое применение на данном уровне развития.


  1. mixsture
    26.01.2025 10:32

    Implement only the minimal code necessary to pass the provided tests.

    Так точно, капитан!
    if (we_in_test_environment) {
    do_behavior_to_do_green();
    } else {
    do_any_crap_behavior();
    }

    А чтобы выйти из этой неприятной ситуации нужны не юнит-тесты с мок объектами, а полноценные интеграционные - которые писать кратно сложнее, окружение для них кратно тяжелее и выполняются они кратно дольше. Так что картинка про отладку в 24ч кода нейросети актуальна как никогда.


  1. sshikov
    26.01.2025 10:32

    до сих пор нет единого мнения о ее эффективности.

    Нет, не так. Нет доказанных измерений ее эффективности. Вот вы же их тоже не привели, не заметили?

    Т.е. типичная статья, агитирующая за применение TDD, выглядит как-то так: "А у меня получилось повысить эффективность при внедрении TDD". А я смотрю на это, и понимаю, что никаких выводов про свою работу и свой проект сделать не могу. Потому что те задачи, которые призвана решать методика TDD, я давно и достаточно успешно решаю другими способами, и мне не нужен тест, чтобы описать, что должен делать код - я это способен держать в голове. Ну или я сразу пишу кусок кода, который будет пользоваться другим куском кода - вместо теста.

    Разумеется, я это не интерполирую на других, кому-то другому методика может быть помогает. Но я без измерений ее внедрять не буду. А измерять производительность программистов, так чтобы это измерение было применимо к другим программистам, другой квалификации, опыта и т.п., почти никто не умеет, потому что это тупо сложно.


  1. danilovmy
    26.01.2025 10:32

    Удивительно, как люди советующие самим летать на AI-разработанных самолетах пользуются бытовыми приборами собранными на полностью автоматизированных производственных линиях, ах, да, это же другое...

    А теперь по делу. Разработкой TDD с AI пользуюсь довольно давно. И прийти к этому довольно просто, надеюсь, что даже самые отъявленные скептики, вероятно задумаются.

    Грубо про TDD - есть четко представленная идея / тех задание. Пишем тест на тех задание, представив, что реализация уже есть. После написания теста, пишем реализацию проходящую тест.

    Удивительно, но есть работающий пример, как создания теста под "тех задание" так и создания собственно самого кода: это код-генератор клиента и сервера почти на на любом языке по описанию из yaml. Сомневаетесь что это работает? - зайдите на editor.swagger.io

    Из текста в код... не находите аналогий со статьей?

    Далее. проще. Задача написать хорошую yaml-схему. Тоже довольно просто, поскольку есть отличные YAML валидаторы и какая-никакая спека OpenAPI (кстати новая версия вышла, Arrazo называется) для проверки корректности смысла схемы.

    А вот тут LLM просто прекрасно справляются. Поскольку это не код и yaml-схема не может работать или не работать. Это текст. Проверить корректность описания endpoint-a довольно просто визуально.

    В итоге в моем примере, LLM, плюс линтеры по тех заданию клепают yaml, а после какой-нибудь connexion генерит код. В моей работе я не могу вспомнить примеры с ноября 2023, когда это не cработало. Тогда Мы/Я в команде начали использовать codewhisperer/codeium для этого.

    Да, немного не в стиле идеи статьи, поскольку у нас по тех заданию сразу генерится клиент сервер приложение, а не тесты. Сейчас уже 420 api, работают как часы. Продолжим.

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

    Так вот. Вернемся к статье.

    Машина не должна писать тесты. Тесты - зона ответственности программиста. 

    Это утверждение я поставил под сомнение: оказывается, машина может писать тесты. Точнее llm пишет задание генератору кода, а с лета 2024 агент научился запускать кодгенератор и предлагать граничные условия для тестов, часто более качественно или подробно, чем это сделал бы я. Например https://github.com/django/django/blob/main/tests/utils_tests/test_numberformat.py - набор случаев созданных человеком ранее не включал большое отрицательное число записанное через e.форму. Уже поправили.

    Ну так вот, по поводу писать тесты самому: мой промпт на TDD обсуждался в чате одного из Code-assistant. выглядит примерно так:

    Please generate TDD tests for Django project according to this specification:

    Дальше текст тех задания на разработку.

    Да, предварительный контекст для модели тоже используется, больше похож на codestyleguide проекта и мои личные предпочтения по коду (например, на питоне писать генераторами без использования [] ). И на мой взгляд, наш ассистент справляется очень хорошо: Тесты работают сразу, граничные условия более продуманные, я часто потом уменьшаю их количество, типа - "тут одного хватит"

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

    Верить или нет коду от LLM, конечно, решать в итоге разработчику. Но вот сократить время разработки LLM точно могут. И кстати, на мой взгляд, с LLM подход TDD, скорее всего, станет не нужен.


    1. arantar
      26.01.2025 10:32

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


      1. danilovmy
        26.01.2025 10:32

        ну если совсем "душнить", то я сравнивал результаты производства (самолет и бытовые приборы) не способы. Так вот. В автоматизированных линиях есть допустимый процент брака в районе 5%. Потому гарантированность одного и того же результата в районе 95%.

        Если с AI я достиг того же - одинаковый результат в 95% случаев - то инженерная задача решена. Или нет?


  1. Dron007
    26.01.2025 10:32

    AI бояться - на BMW не ездить. Там уже давно тестируют роботов.