Как стать автором
Обновить

Комментарии 48

Есть ли кардинальная разница между "написать тест -> спроектировать модуль -> довести модуль -> изменить тест" и "спроектировать модуль -> проверить прототип -> довести модуль до рабочего состояния с учетом всех новых данных -> написать тест"?

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

Есть мнение, что тесты, написанные после модуля, будут проходить с большей вероятностью, чем тесты написанные заранее.

Да и я склонен верить, что самостоятельное тестирование собственного приложения после его написания, выявит меньше потенциальных багов, чем обратная ситуация.

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

да. Если вы приступаете к работе не зная все особенности, подводные камни, то вы шляпу а не тест напишите.

А если вы приступаете к работе наперед зная все детали - тут вопрос однотипных задач

То, что ставится на первое место, чему уделяется основное внимание - то и получается качественнее. Практикуя TDD мы получаем качество тестов выше качества кода. Ещё бывает Documentation-driven development - там та же проблема. Вряд ли это то, к чему стоит стремиться.

Немного не так. Тесты спереди часто дают на выходе более у добное и просто api у бизнес логике. Просто потому, что в момпнт написания тестов становятся видны моменты где оно не удобно и что лучше поменять.

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

По-моему TDD многие попробовали лет семь назад и отказались от этого подхода. Возможно потому, что неправильно его применили, не знаю. Бывает такое, что пишут тесты, потом код, и осознают, что тесты нужно переделывать. Получается, в этом случае тесты пишутся дважды. Концепция BDD(Behavior-driven development) выглядит более привлекательной и на практике даёт лучше результат. Она как-то лучше прижилась.

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

На текущем проекте перебиваюсь valgrind, gcc с максимально выкрученными warning'ами, cppcheck, clang-tidy, фаззингом и автогенерируемыми тестами (chatgpt). Это всё тоже даёт какую-то уверенность, хотя и не непробиваемую.

Разработка через тестирование это примерно как трусы через голову надевать.

Классчиеский спор остро- и тупо- конечников. С какого конца начинать, зависит от ситуации. А иногда надо и с обоих. А иногда и ещё с другого, третьего, четвёртого конца. Наука умеет много гитик.

А вот почему не пытаются ИИ заставить писать код, прогоняя ответы через TDD? Вот где TDD действительно был бы полезен.

Не знаю, как на уровне индустрии, а я таким занимался ещё на заре chatgpt: заставляем писать код с тестами, затем скармливаем результаты тестов и заставляем править код. Проблема лишь в том, что без программиста всё равно не получается: у LLM очень быстро возникает кризис идей, и нужно подсказывать и направлять, куда развивать дальше. Или иногда он упирается рогом, доказывая, что его код истинно верный, а весь остальной мир ошибается. Проще ли эта возня, чем самому написать? А вот фиг его знает, зависит от задачи. Ну, и контекста всегда катастрофически не хватает, каким бы большим он ни был :D

Эм. Так только так и делают ведь. Делают запрос, сверяют ответ с эталоном, не понравился - меняют веса на сетке, и продолжают цикл пока не получат удовлетворяющий результат.

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

Далеко не все разработчики хорошие. 95% скорее средние и практика извне - чуть ли не единственный способ поднять качество проекта.

Вы забываете, что Хороший Тру-Программист это всегда в первую очередь Творец. Сама идея что он будет писать тесты это кощунство и анафема. Разве Пикассо когда-нибудь писал тесты на свои шедевры? :)))

Настроение автора и выводы понятны без их письменного документирования. Статья попытка оправдаться перед собой и возможно перед коллегами. Но действительность такова что TDD надо внедрять. Конечно нужен опыт.

  • «TDD подталкивает разработчиков к более прагматичным решениям»: В рамках ООП- шного языка программирования TDD тесно сотрудничает с SOLID и со всеми соседними прагдигмами. Требует знаний паттернов проектирования. Да и вообще ставит мысли на место.

  • Рефакторинг рано или поздно нужен всем. Как быть уверенным в том, что после рефакторинга можно вообще что то отдавать в прод? Трата времени на тесты после этого - отсутствие седых волос при внедрении. А если приходят новички? Как до них донести крайние тест-кейсы кроме как заранее не занести их все в UT?

  • «TDD способствует четкому пониманию требований до начала написания кода»: не понятно о чем речь. Не знаешь что писать, не начинай пока не разберешься

  • «Само по себе наличие каких-либо тестов, как я выяснил на практике, не гарантирует ровным счетом ничего.» Просто писать тесты ради тестов? Зачем? Если не понимаете сути то, конечно, лучше их не писать до тех пор пока в этом не разберетесь.

  • «Огромное количество тестов, с которыми сталкивался я, не были в состоянии ответить ни на подобный, ни на прочего рода вопросы.» Ну так это не аргумент, а просто факт что те тесты что вы смотрели были ужасными. Мотивировать их не писать он не может. Только наоборот.

  • «TDD отнимает время»: так в любой сфере. Вопрос: чье время и сколько оно стоит? Из опыта: «затраты» потраченное на тесты потенциально меньше разного рода суммы «затрат» на разных следующих этапах включая внедрение.

  • «Тесты необходимо поддерживать при изменении требований»: это преимущество при хороших тестах и минус при плохих. Не более не менее.

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

Поинт с рефакторингом не валидный. То, что тесты нужны перед рефакторингом - это факт, но это не значит, что тесты нужны до первой версии кода.

«в реальной жизни фичи устроены немного сложнее, чем «функция X должна принять имя и вывести приветствие с этим именем»
Отсюда следует два вывода:

  1. Либо аналитика работает настолько криво, что задачу невозможно нормально декомпозировать

  2. Либо разработчик настолько некомпетентный, что просто не умеет в тесты.

Из своей практики скажу, что один из минусов TDD это высокий порог вхождения - нужна как зрелая команда, так и личная самодисциплина в целом. Кто и раньше писал тесты "тяп-ляп", тому и тдд не поможет.

Я вообще не представляю как кто-то может писать без тестов. Потому что я вижу только две альтернативы.

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

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

Разговор не про то "писать тесты или нет", а про порядок написания кода и тестов. Часто во время разработки пишется код и тесты одновременно.

Часто во время разработки пишется код и тесты одновременно.

Я всегда так и делаю. Единственное где я иногда пишу тесты вперед - это на контракт метода (т.е. например на проверку входных параметров). Например, делаю пустой метод, потом пишу тест, что он проверяет нужные параметры на null, потом реализую в начале метода эти проверки. А все остальные тесты уже по ходу реализации остальной части метода. Личный опыт показывает что если при этом еще непрерывно вести контроль покрытия (в случае .NET это, например, "coverlet" и ".NET report generator" для визуального просмотра покрытого и непокрытого кода), то все выходит отлично даже без "ортодоксального" TDD.

Hidden text

Аргумент "TDD отнимает время" и согласие с ним "сторонников TDD" приведены некорректно. В источнике написано "TDD занимает больше времени на краткосрочных проектах. На долгосрочных наоборот показывает выигрыш в качестве кода и скорости разработки". Кажется, в большинстве случае это аргумент "за", а не "против" TDD.

Я бы указал на еще один аспект. Юнит-тесты заставляют писать более качественный код. Если код нарушает принципы (в порядке важности) "Dependency inversion", "Single responsibility", и "Interface segregation", либо имеет высокую "cyclomatic complexity" то тесты к нему писать крайне тяжело. Это, кстати, еще одна причина чтобы писать тесты если не вперед, то как минимум сразу. Если это дело откладывать на потом, то очень возможно что потом для тестов придется еще и сам код рефакторить.

Разброс прироста к длительности процесса разработки составляет 10-35%, в зависимости от квалификации и опыта команды.

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

В результате продукт получается неконкурентоспособный на рынке, если это не хелловорлд, конечно, с десятком тестов. Имхо, TDD - это такая мина от корпораций под стартапы, что хотят сделать все красиво и идеально.

За 15 лет не видел ни одной компании, которая бы использовала TDD. Ни энтерпрайз, ни стартапы (проекты в России, США, Германии, Великобритании, Чехии). TDD продвигается на индивидуальном уровне TDD энтузиастом (как правило джун-мидл, работающий в своей первой-второй компании). Я не знаю ни одного сеньора, который придерживается этого подхода (не путать с покрытием тестами). TDD катастрофически снижает time-to-market, ухудшая качество кода, в сравнении с подходом code first. Большинство сеньоров+ принимают решение сверху, от архитектуры. Среди них наиболее распространенный подход - Domain Driven Development (DDD).

P.S. Сеньор с опытом меньше 5 лет это оксюморон.

Вы говорите про интеграционное тестирование, но в TDD фокус на юнит тестах. Если вам нужно 9000 моков, чтобы написать простейший тест, то наверное, что-то не так с проектированием системы. Возможно, если тесты писать до кода, то моки могут и не понадобиться.

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

По поводу генерации данных для тестов - есть такой подход как Property Based Testing.

По поводу генерации данных для тестов - есть такой подход как Property Based Testing.

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

Оно воспроизводимое. Просто добавляется дополнительная морока куда-то сохранять данные, необходимые для их воспроизведения. Особенно когда тесты выполняются на CI.

Да, если сохранять данные, то это вариант. Но, вот то что я д сих пор до этого видел, там ничего такого не было - тесты просто гонялись на случайных данных.

Зачем сохранять данные, если, например, можно хранить seed для генератора?

На порядок время отрастает.

Если делать с одним и тем же качеством то ровно наоборот.

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

  2. Провальный тест даёт моментальный отклик на кучу дурных ошибок (написали < вместо >). Исправить её прямо сразу это моментально, исправлять её даже всего лишь через час это уже в разы накладней, а если эта ошибка уехала в QA, а то и вообще в production, то разница в трудоемкости её исправления возрастает на порядки (особенно если это еще и не твоя собственная ошибка).

"Неправильно ты готовишь TDD, дядя Фёдор" © Простоквашино

TDD отличная методология программирования. Сначала пишешь заготовку функции или класса, так, чтобы она удовлетворяла требованиям, а потом обвешиваешь её Unit тестами. В процессе обвешивания тестами выясняется, что тут что-то работает не так, там выдаёт не то. Ну и правишь код. В результате, за то же время получаются и функция/класс и Unit тесты к ней/к нему. Никакого замедления разработки. Если впоследствии нашлась ошибка, воспроизводишь её в Unit тесте и исправляешь.

Господа минусующие, ну хоть бы просветили, в чём я заблуждаюсь. А то ставите минус и молчок. Может я действительно что-то упускаю из виду. Буду рад критике.

То что вы описали это не TDD. По канону тесты пишутся до функциональности. У вас получилось какое угодно DD просто с покрытием тестами (против чего собственно возражений нет).

В процессе обвешивания тестами выясняется, что тут что-то работает не так, там выдаёт не то. 

Вот такое по TDD в принципе не возможно. Тесты считаются single point of truth, и функциональность не считается законченной пока есть хотя бы один красный тест.

Вы все возможные ситуации можете предусмотреть? Если да, то Вы очень прозорливый человек. Я нет. Сами знаете, что программ без ошибок не бывает. А если есть ошибка, то значит не все возможные ситуации удалось предусмотреть.

Я сначала пишу базовый функционал, который хоть как-то работает, а потом обвешиваю его тестами, которые чаще всего проваливаются и далее соответственно дописываю функционал. Да, тут я не в полном согласии с парадигмой, но так, ИМХО, быстрее.

Почему бы не писать тесты параллельно с функциональностью, когда функционал становится более ясен? Неужели на столько важен порядок разработки?

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

Ок, не буду, назову это чем-то вроде «итеративного TDD» 😉

На самом деле штука не плохая, пользуюсь постоянно и ошибки в коде тоже через подобную методику воспроизвожу и исправляю (а это вроде как даже часть TDD). На скорость разработки почти не влияет (Тестировать написанное ведь всё равно надо, а основная часть времени уходит на тестирование – с этим, думаю, Вы тоже согласитесь).

Тесты к несуществующему классу это действительно что-то эзотерическое.

UPD: и не "по необходимости", а "по мере фантазии". Всё равно знаю, что закрою не все возможные варианты.

Могу привести пример TDD из моей практики.
Есть такой DSL - Apache Gremlin. Это как SQL, но только к графовым структурам.
Я делаю имплементацию этого DSL для Rust.
И тут как. Беру в реализацию шаг. hasId(), к примеру. Сначала пишу тесты на то, как он должен работать. В случаях успеха и ошибок. Тесты эти по началу вообще не компилируются.
Основные тест-сценарии беру из исходников Gremlin'a чтобы также проверять совместимость со спецификацией.
Потом уже пишу код.
Поэтому тесты к несуществующим "классам" в моем случае это не что-то эзотерическое :)

Это довольно узкий кейс, когда реализуется поддержка уже существующего формата/протокола. Он отличается тем, что во-первых требования известны заранее и не изменятся в процессе работы, и во-вторых тесты нередко уже есть и их можно взять готовые или конвертировать (автоматически или вручную). В таких условиях вполне норм написать тесты до кода. Но к TDD этот специфический кейс имеет примерно никакое отношение - формально похоже, но суть заметно отличается (тем, что тесты, в общем случае, вообще к конкретной реализации не привязаны).

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

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

Для меня TDD примерим в двух случаях: 1) как в примере выше, когда есть четкие, стабильные спецификации, 2) внутренняя core часть проекта к которой есть четкие требования.

Делать бизнес функционал через TDD не имеет смысла, имхо.

Рабочий и относительно здравый вариант TDD для бизнес-функционала называется BDD. В теории. Потому что на практике я лично его в стоящих упоминания масштабах не встречал.

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

Вам ещё повезло, мне сейчас приходится рефакторить легаси на Матлабе. Да, Unit и интеграционные тесты им уже завезли, но как же всё в Матлабе криво...

TDD не гарантирует качество тестов

Ничто не гарантирует качество тестов. По-моему, это не характеристика именно TDD - это просто так и есть. Вне зависимости.

Изначальные требования могут быть поняты или сформулированы неверно

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

Ничто не гарантирует качество тестов.

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

Hidden text
  <PropertyGroup>
    <CollectCoverage>true</CollectCoverage>
  </PropertyGroup>
  <-- То что ниже вообще из стандартного шаблона проекта добавляется по умолчанию -->
  <ItemGroup>
    <PackageReference Include="coverlet.msbuild">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории