0,2
рейтинг
22 августа 2013 в 14:43

Разработка → Почему изучать TDD трудно и что с этим делать. Часть 1 из песочницы

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

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

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

Почему же это так трудно?


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

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

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

*Некачественный код выполненным заданием не считается.

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

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

Пойми технику, затем научись применять ее


Я думаю, хорошим способом облегчить изучение TDD является раннее фокусирование на том, как эта техника работает. Конечно, стоит начать с простейших примеров разработки классов вроде Stack или StringCalculator, чтобы усвоить процесс «красная полоса – зеленая полоса – рефакторинг». Но после надо остановиться и подумать над тем, что, собственно, это процесс делает.

Мы начинаем с написания несрабатывающего теста. Зачем? Существующие тесты проходят нормально, значит, в коде нет известных проблем. Создав тест для выявления недостающего функционала, мы четко выявляем задачу, которую собираемся решить. Фиксируя задачу и, следовательно, ее возможные решения мы делаем процесс работы над ней более простым. Мы так же гарантируем, что результирующий код будет покрыт тестами на правильность реализации и защищен от регрессии (хотя этого можно достичь и без TDD с помощью любого достаточно глубокого автоматического теста).

Но самым важным здесь является создание наброска будущей архитектуры. Просто делается это не на доске для рисования (что имеет свои преимущества), а прямо в коде, что позволяет незамедлительно понять, насколько просто использовать и тестировать этот код. Посмотрим, какие вопросы возникают при создании самого первого, несрабатывающего теста:
  • В чем заключаются обязанности тестируемой системы (в оригинале SUT, System Under Test – прим. перев.)? Иными словами, что и когда она должна делать?
  • Какой API удобен для того, чтобы тестируемый код выполнял задуманное?
  • Что нужно тестируемой системе для выполнения своих обязательств (данные, другие классы)?
  • Что мы имеем на выходе и какие есть побочные эффекты?
  • Как узнать, что система работает правильно? Достаточно ли хорошо определена ли эта «правильность»?

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

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

Процесс создания теста дает всестороннюю оценку разрабатываемому модулю. Подготовка к использованию чересчур громоздка? Возможно, у нас слишком много вспомогательных классов (или они нарушают Закон Деметры) и можно попытаться скрыть их за дополнительной абстракцией. Получается многовато сценариев использования или фикстур? Вероятно, тестируемый код имеет слишком много обязанностей. Трудно изолировать тестируемое поведение или непонятно, как его проверить? Наверное, у нас неправильное API или сама абстракция, и ее надо выделить как-то иначе. С TDD эти проблемы становятся очевидными мгновенно. Их решение требует навыков проектирования, таких же, каких требуют другие методики разработки. Но создание теста в самом начале дает отличную возможность отреагировать на ошибки и проверить проект модуля до его реализации. Самое дешевое время для исправления кода – до его написания.

Затем мы пишем самый простой код, удовлетворяющий тесту. Как ни странно, это наименее интересная часть TDD. Мы уже закончили всю тяжелую работу по выявлению требуемого поведения, и сейчас осталось только реализовать его. Реализация слишком сложна? Тогда придется вернуться назад и изменить тест – мы только что узнали с помощью TDD, что попытались сделать слишком большой шаг в разработке модуля. Реализация тривиальна и имеет очевидные недоработки? Отлично, теперь мы знаем, каким будет следующий тест.

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

Более конкретно о том, что со всем этим делать – в продолжении.
Александр Мозговой @velvetcat
карма
16,0
рейтинг 0,2
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +23
    Очередная бесполезная статья по TDD. Одна вода.
    • –1
      Можно более полезно конкретно? Буду рад услышать конструктивную критику.

      Сразу объясню свою позицию. Я думаю, что «полезной», выводящей сразу в дамки статьи по TDD быть просто не может. TDD требует серьезной работы (над собой) по многим фронтам. Этот перевод появился именно потому, что автор долго и со вкусом ищет причины неуспеха TDD у большинства разработчиков. Здесь не может быть цифр, паролей и явок — таких поверхностных статей много и все они вызывают такую же поверхностную реакцию — «попробовал, не получилось, забил, кричу на всех углах, что это не может работать вообще».
    • 0
      Люди — разные.
      Если Вы тонете в этой «воде» — это не значит, что все остальные не умеют чувствовать себя как рыбы в воде, читая подобные абстрактные «общие соображения». Возможно, в то же самое время кто-то наоборот — изнывает от жажды в песках конкретики, и ему именно эта вода и нужна.
  • –2
    Обоснование использования TDD для заказчика довольно проблемный процесс. Люди просто не понимают, почему сразу нельзя написать правильно.
    • +11
      Спрашивать разрешения у заказчика, использовать ли TDD? Шутите?
  • +4
    Очень трудно с TDD на практике. Допустим, я какое-то время изучал эту технику, прочел несколько хороших книг, написал пару-тройку игрушечных проектов и т. д. Но вот на горизонте настоящий проект. Худшее, как правило, то, что раз уж он новый — нет хорошего знания предметной области. В такой ситуации рефакторинг дело очень частое. И вот я перелопачиваю гору кода при каждом чихе, а вместе с этой горой переписываю тесты. Причем тестов раз в десять больше чем кода, и их нужно редактировать в первую очередь, перед самим кодом. Работы получается во много раз больше. Что с этим делать я пока не предумал, и пока TDD не использую. Может местные гуру посоветуют что-нибудь?
    • 0
      Вы сами ответили на свой же вопрос: нужно хорошее знание предметной области. Попробуйте начать с диаграммы предметной области, а не с кода. Или просто со списка юзкейсов. На этом этапе должно отсеяться много потенциально выброшенного в будущем кода.
      • 0
        Из поста:
        Но самым важным здесь является создание наброска будущей архитектуры. Просто делается это не на доске для рисования (что имеет свои преимущества), а прямо в коде.


        В книгах, в общем-то, тоже пишут что дизайн следует из тестов, а не наоборот.
        • 0
          Я говорю не про архитектуру, а про предметную область. Это выше уровнем, чем архитектура проекта. Классы нельзя начать проектировать, пока непонятно, что вообще будет делать программа.
          • +1
            OK. Я понял о чем Вы мне говорите. Допустим, я разобрал предметную область по кирпичикам, пускай даже вместе с экспертом в этой области. Но ведь классы не ложатся на модель «один к одному». После этого я немного переработаю диаграмму, чтобы наложить её на язык программирования. Ещё 100% будут ошибки в модели, коррекция и т. п. Остается много кода и тестов для переписывания.
            • +1
              Вот здесь часто кроется ошибка — в модели делать классы, соответствующие физическим объектам предметной области один к одному. Классы должны соответствовать абстрактным сущностям предметной области. Конкретный сферический пример: если Вас у есть Клиенты, это вовсе не значит, что они должны представлять из себя физ- или юрлиц со всеми их атрибутами. Клиент — это абстрактная роль. А обычно делают наоборот — создают класс физ/юрлицо, с флагом «клиент» или «поставщик», и начинают тр… ся с этими двумя совершенно разными сущностями, засунутыми в один класс.

              И TDD здесь помогает тем, что такие кривые абстракции становятся видны на раннем этапе.
              • 0
                Клиент — это конкретная роль. Физлицо — конкретный объект. Вот только нужно не ставить флаг, а назначить физлицу роль Клиент. Иногда можно флагом (простейший случай композиции), но чаще лучше сразу агрегированием.
    • 0
      Я не гуру, и мой опыт в написании тестов очень небольшой. Но я сделал для себя вывод, что бОльшую ценность имеют функциональные (приёмочные) тесты, а не модульные.
      То есть, мы тестируем не отдельные функции, а пишем, например, что:
      1. страница по такому урлу должна вернуть код 200;
      2. на ней должна быть форма;
      3. форма при заполнении невалидными данными должна вернуть ошибку (и какую);
      4. форма при заполнении валидными данными должна быть обработана, должен произойти редирект и на новой странице должны быть определённые данные.

      Согласитесь, код может меняться, а внешнее поведение меняется намного реже.

      А модульными тестами я предпочитаю покрывать уже более или менее стабильный код, который сильно меняться не будет.
    • +1
      Напишите сначала без TDD. Полученный код считайте прототипом, не подлежащим отправке в продакшн. Перепишите начисто с TDD.
    • +1
      Смотрите, вы же согласны, что любое приложение нужно разбивать на модули/компоненты. Ну а что бы разработать какой либо компонент/модуль, его же нужно как-то «дебажить». И как обычно делают без TDD? Создают классы, и сразу подключают в приложение и уже вместе с приложением разрабатывают тот самый модуль, изменили код «кирпичика» — перезапустили приложение. Или так уже никто не делает? Если нет — то хорошо, потому что более удобный способ как раз TDD, или что то сравнимое с ним. Дела ещё усугубляется языком — мой основной на данный момент javascript. И здесь без TDD, ещё сложнее — здесь сплошной «контракт» — нет никаких типизаций, интерфейсов, сигнатур функций. Как раз тесты будут всегда держать модули и их связи целостными. Но вернёмся к разработке — мы даже свою библиотеку тестов создали, что бы было проще разрабатывать. Таким образом разрабатывается не приложение, а модули по отдельности, а тесты перенимают роль «мини-приложений» для дебага. И как раз эта особенность «мини-приложений» делает TDD таким привлекательным в разработке.
      • если вы не можете определиться с конечным Интерфейсом модуля, можно начать с функционала, а там уже и интерфейс модуля отшлифуется, и можно подключать в приложение.
      • каждый раз, когда обнаружите ошибку в программе, достаточно найти модуль/компонент который ломает приложение, ввести «входные» данные в его тесты и продолжить разработку/ремонт модуля.


      Это как дом строить — можно или свезти все детали на стройку и там же создавать кирпичи, отливать плиты и прочее, а можно это все сделать отдельно на разных заводах (читать тестах) и потом привезти уже готовые элементы. И не построить — а собрать дом.
    • +1
      И вот я перелопачиваю гору кода при каждом чихе,

      Если Вы не утрируете, то это железобетонное свидетельство того, что с архитектурой что-то не так (скорее всего, ее просто нет), а проект серьезно болен, независимо от того, насколько сильно изменения связаны с проблемной областью.
      • 0
        Кстати интересный момент, а если «перелопачивать гору кода» приходится по причине того, что внезапно изменились требования. Начинать с начала? Врядли. Изменить код/тесты за один заход я не могу, ибо слишком большой шаг. Постепенно менять будет сложно. Ибо хоть я и понимаю разницу между имеющимся функционалом и тем, который должен получиться в результате изменений, я все же не могу представить себе цепочку маленьких шагов, которые должны быть сделаны для достижения цели.

        • 0
          Eдалить мешающие тесты и покрывающий их код, после чего начать с новых тестов. Или отказаться от TDD.
        • 0
          Анализируете, какие классы требуют изменений, затем переписываете их, используя TDD. Если у Вас получается, что для реализации одного изменения требуется изменить много классов — у Вас неправильная архитектура.

          Помните, цель любой архитектуры — сделать приложение гибким (в противовес хрупкому), в котором каждая часть отвечает за что-то одно, и эти части могут быть изменены по-отдельности без необходимости модификации их соседей. TDD выявляет корявость архитектуры, не позволяя обходиться костылями.
          • 0
            А ведь часть тестов при этом ведь начнет фейлиться, то есть нужно еще и проанализировать какие тесты удалить (или временно задизейблить) до того, как я напишу «первый» тест для нового функционала. Я правильно понимаю?

            • +1
              Не совсем так. Тесты не начинают фэйлиться — в исходном состоянии все ок. Затем мы добавляем новый (или изменяем старый) тест, который описывает новый функционал, он, естественно, падает. Реализуем.

              Это — идеальный вариант, когда выбрав очередное изменение, можно найти единственный класс, который надо изменить. В реальности, конечно, будут шероховатости, и тут тесты сыграют свою вторую роль — контроля на регрессиями.
            • +1
              Фэйл теста означает, что что-то из «Анализируете, какие классы требуют изменений, затем переписываете их, используя TDD» вы не сделали. Как правило это означает, что система не поддается анализу.
    • 0
      В такой ситуации рефакторинг дело очень частое. И вот я перелопачиваю гору кода при каждом чихе, а вместе с этой горой переписываю тесты.

      При рефакторинге тесты рефактируемой части не переписываются. Их во многом и пишут для того, чтобы проверить, что при рефакторинге функциональность не изменилась.
    • 0
      Так если много рефакторинга, тесты как раз очень полезны. Ацки полезны! Они позволяют мне быть уверенным, что мой рефакторинг ничего не сломал. Иначе появляется самая страшная болезнь программеров — боязнь изменений. Все эти «работает — не трогай» от начальства.

      Вы боитесь, что придётся каждый раз переписывать слишком много тестов. В теории не придётся. Поскольку каждый тест должен тестировать только один аспект, то при изменении одного требования придётся переписать один тест. Это в теории. А как оно получится на практике — всё в вашим руках.
  • 0
    почему TDD трудно:
    1. количество тестов в 10 раз превышает объем тестируемого кода.
    2. изменения кода влекут переписывание многих (а то и всех) тестов.
    3. мнообразие тестов (скажем, форма из N полей, сводится к 2^N тестам), 3\4 из которых очевидна (как только начинаешь писать тест, уже видишь, код содержит баг или нет).
    4. тесты на специфичные баги: нашли баг, добавили тест, исправили, тест проходит, отлично; но погодите, а в других местах нет ли этого же бага?
    5. трудно придумывать нужные тесты: чаще, при разработке, мы даже не задумываемся, что возможны такие условия, при которых система может упасть
    6. сложность эмуляции окружения сложного (а особенно сильносвязанного) приложения: нужно создать mock-БД, наполнить верными и заведомо неверными данными, поднять фреймворк или эмулировать его часть
    6. многообразие типов тестов: не юнит-тестами едиными, нужны и интеграционные тесты, и автоматизированные тесты…
    7. тесты не улавливают магические ошибки, которые связаны с бубном и фазами луны: нужна реальная ситуация с реальными данными, чтобы хотя бы догадаться, как составить тест, а уж чтобы он (не)проходил…
    8. концепция TDD требует тщательного проектирования: если мы тесты пишем до написания кода, а детали реализации не видны до рабочего прототипа, то у нас проблемы.

    В итоге, кроме приличного багажа знаний, TDD требует хороший скилл предвидения и трудозатрат… Что сильно отпугивает.
    Так, я пришел от подхода TDD к подходу Defensive Programming: начиная проектировать, я пишу код, а вместе с ним, сразу мысленно тесты и фиксы, чтобы эти мысленные тесты код мысленно проходил — без реального написания тестов. Впрочем, такие крайние ситуации стали заметны с накоплением приличного опыта, а это тоже на дороге не валяется…
    • +1
      два раза №6

      Некоторые моменты существуют и без TDD
      2. Тесты не должны полагаться на знание внутренней логики тестируемого субъекта. Если спецификация не меняется, тогда рефакторинг не должен ломать тест. Если поменялась спецификация — тогда это фактически новый код.
      4. То есть в других местах есть копированый код?
      5. Любые тесты должны минимум покрывать спецификацию и граничные условия. Всё предусмотреть невозможно. Вон в жаве нужно ловить или заявлять все исключения а толку особо нет.
      6.1 Как раз здесь становится понятно, что дизайн неудачен. Если поменять нельзя и код не будет развиваться — здесь стоит вообще отказаться от покрытия. Если код сильно связан, то он не только плохо тестируется, он и развивается плохо.

      Но, в общем, согласен. Трудозатраты при ТДД не всегда оправданы.
    • +6
      Спасибо за прекрасную коллекцию заблуждений. Ниже по пунктам.

      >1. количество тестов в 10 раз превышает объем тестируемого кода.

      Да. Но этот код примитивен. Если это не так — у Вас плохой дизайн.

      >2. изменения кода влекут переписывание многих (а то и всех) тестов.

      Нет, если Вы грамотно выделили классы.

      >3. многобразие тестов (скажем, форма из N полей, сводится к 2^N тестам), 3\4 из которых очевидна (как только начинаешь писать тест, уже видишь, код содержит баг или нет).

      1. Вообще-то речь о создании тестов до кода. 2. Спустя некоторое время все станет не столь очевидным. 3. Неужели Вы не видите, что именно этот пример отлично демонстрирует эффективность тестов?

      >4. тесты на специфичные баги: нашли баг, добавили тест, исправили, тест проходит, отлично; но погодите, а в других местах нет ли этого же бага?

      Причем тут вообще тесты? Вы допустили дупликацию и справедливо несете за это наказание :)

      >5. трудно придумывать нужные тесты: чаще, при разработке, мы даже не задумываемся, что возможны такие условия, при которых система может упасть

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

      >6. сложность эмуляции окружения сложного (а особенно сильносвязанного) приложения: нужно создать mock-БД, наполнить верными и заведомо неверными данными, поднять фреймворк или эмулировать его часть

      Это не настолько сложно. Если сложно — у Вас лапшекод. Ну, или если хотите, «сильносвязанное приложение».

      >6. многообразие типов тестов: не юнит-тестами едиными, нужны и интеграционные тесты, и автоматизированные тесты…

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

      >7. тесты не улавливают магические ошибки, которые связаны с бубном и фазами луны: нужна реальная ситуация с реальными данными, чтобы хотя
      бы догадаться, как составить тест, а уж чтобы он (не)проходил…


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

      >8. концепция TDD требует тщательного проектирования: если мы тесты пишем до написания кода, а детали реализации не видны до рабочего прототипа, то у нас проблемы.

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

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

      Ну, вообще-то о больших требованиях к разработчику и сказано в статье. Но разве это не стимул развиваться?

      >… пишу код, а вместе с ним, сразу мысленно тесты и фиксы, чтобы эти мысленные тесты код мысленно проходил — без реального написания тестов

      А потом прогоняете эти тесты тоже мысленно?
      • +2
        «Если это не так — у Вас плохой дизайн.»… «Нет, если Вы грамотно выделили классы. »… «Если сложно — у Вас лапшекод.»…
        Звучит так что TDD для таких «белых воротничков», но я не всегда могу похвастаться хорошей продуманной архитектурой, правильным выделением классов и прочими best practicies.
        Очень хочется перейти на TDD, DDD и прочие вкусности, но в продакшен проектах со сжатыми сроками страшновато начинать экспериментировать.
        Как вы пришли к TDD? На своих личных проектах начинали?
        • 0
          но я не всегда могу похвастаться хорошей продуманной архитектурой, правильным выделением классов и прочими best practicies.

          Если начинать с тестов, то они зачастую «заставляют» делать хорошую архитектуру, правильное выделение классов и т. п.
        • +3
          Да, на своих, когда осознал, что все время до этого я не программировал, а тупо фигачил как придется. Появилось острое чувство собственного непрофессионализма и сильное желание от него избавится.

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

          Тут уж Вам выбирать — слушать тех, кто мимо пробегая вывалил результат типа «у меня не получилось, так что все это абсолютно нереально и не может работать, вот исчерпывающий список коренных недостатков», или тех, кто промучившись год, начинает, наконец применять эту технику и считает ее стоящей потерянного времени.
          • +2
            Начинать на проектах со сроками не получится.


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

            С этого вообще нужно начинать во всех красивых методиках. И чтобы не было потом в них разочарования, нужно понимание того как много придется за красоту заплатить, и доживет ли проект до результатов.

            В сети очень мало материалов о реальных проблемах внедрения TDD — организационных, финансовых. Нужны способы их решения, а не плюсы TDD. О плюсах TDD и так все знают. А вот как преодолеть минусы никто не рассказывает. Вместо того, чтобы дать конкретные советы типа «нету 2-3 лет опыта программирования — не суйся с TDD», «срочный проект — без TDD, во всяком случае на первом этапе», «заказчик неистовствует от идеи увеличения сроков — дайте ему посмотреть такие и такие исследования об эффективности TDD», и т.д.
      • +1
        Спасибо за прекрасную коллекцию заблуждений.

        это похоже на реальные исходные ситуации, с которыми разработчику часто приходится иметь дело. не зависимо от того, кто именно заблудил — он сам или коллеги. :)
        • 0
          Исходные ситуации знакомы. Но мне кажется, что у Lure_of_Chaos что-то не то с подходом к ним.
      • 0
        >>В корне неверно. TDD — это минимальное предварительное проектирование

        Выделяется отдельный класс с соответствующим методом и контрактом, вынесеным в отдельный интерфейс, который будет пробрасываться через прокси-класс, причем сам этот класс имплементирующий интерфейс будет инстанцироваться специальной фабрикой с конфигурирацией вынесеной в отдельный xml, который генерируется…

        Вот такое минимальное проектирование. Против «максимального» «a =b + c;».

        А в следующей итерации вся эта красота выкинется на помойку.

        >>Да. Но этот код примитивен. Если это не так — у Вас плохой дизайн.

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

        Ваши аргументы несколько отдают «страной эльфов»:
        — давайте по сравнению с предыдущей увеличим бюджет на фичу в три раза.
        — да вы с ума сошли! вы что, в три раза больше кода напишите?
        — не, в десять, но код там простой, примитивный.
        — а, ну раз примитивный то конечно, тогда хоть в пять раз!
        • 0
          Выделяется отдельный класс с соответствующим методом и контрактом, вынесеным в отдельный интерфейс, который будет пробрасываться через прокси-класс, причем сам этот класс имплементирующий интерфейс будет инстанцироваться специальной фабрикой с конфигурирацией вынесеной в отдельный xml, который генерируется…

          Это явно не TDD, если всё что нужно это сложить или провести иную операцию над двумя числами. Максимум один класс с тремя методами (setA, setB, getC) и то явный ООП головного мозга, если других требований нет, а так — обычная функция или статический метод, если язык не позволяет функции и/или нет нэймпэйсов, а хочется. Тест — одна (исполняемая) строчка типа assertEqual(666, sum(123, 543));, реализация — одна строчка типа return a+b;. Зачем все эти ваши контракты и фабрики? Это если вообще нужно выделять сложение в функцию/метод.
          • 0
            Вы или тролите или никогда не видели ТDD в живую. Поскольку я – неисправимый оптимист, буду считать второе.

            То, что вы назвали “ООП головного мозга” – это описание работы самого обычного DI, верного и незаменимого друга TDD-шника.
            Вы спрашиваете “зачем все эти ваши контракты и фабрики?” Так это не мои, это ВАШИ контракты и фабрики. И если мы не собираемся всё на свете передавать через параметры в эдаком процедурном стиле, то ваша “return a+b” неизвежно превратится в что-то типа:
            SomeDIContainer.GetInstance().GetA() + SomeDIContainer.GetInstance().GetB();

            И за той одной строчкой assertEqual(666, dummyPureEvil.GetDevilSum()) cтоят все эти контейнеры, интерфейсы и прочие километры довольно бессмысленного кода(все эти прокси, которые тупо пробрасывают вызовы наверх и т.д.).

            При “правильном” TDD битва с зависимостями начинается с самого начала и это УЖЕ предварительное проектирование(которого нет в не TDD подходе), причём заранее невозможно сказать, надо оно или нет.
            Т.е. в теории оно как бы надо, а на практике я никогда не видел пользы от избавления от зависимостей типа System.DateTime.Now, например.
            • 0
              То, что вы назвали “ООП головного мозга” – это описание работы самого обычного DI, верного и незаменимого друга TDD-шника.

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

              Т.е. в теории оно как бы надо, а на практике я никогда не видел пользы от избавления от зависимостей типа System.DateTime.Now, например.

              Из недавней практики: при рефакторинге избавлялся от зависимости от rand() и date(), т. к. алгоритмы были на них сильно завязаны и что-то тестировать было не реально. Вернее не от зависимости, а вынес их в отдельные методы, которые в тестах мокал средствами фреймворка.
    • +2
      1. Плохо написанные тесты. Объем тестов более чем в 2-3 раза превышающий объем кода — это уже подозрительно.
      2. Плохо спроектированные тесты. Если изменение кода функции валит тест, не относящийся напрямую к этой функции (не считая приемочных, конечно), то это плохие тесты. Не говоря уже о том, что тест должен быть изменен до изменения функции.
      3. Чаще всего не нужно тестировать каждое поле. Если такая необходимость есть, то либо неправильно спроектирован код, либо тесты. Например, мы проверяем форму на корректный вывод ошибок. Большинство полей типовые, пара штук — с особенностями. Если код спроектирован правильно, ошибки, выводимые на форму, берутся из какого-то объекта (модели или form object). В таком случае достаточно протестировать класс этого объекта на предмет выдачи правильных ошибок, одно-два типовых поля формы (одним тестом) и поля с нестандартным поведением.
      4. Это не проблема тестов и TDD. Даже если вы не пишите тестов вообще, этот баг все равно может быть в других местах.
      5. Опять же не проблема TDD. Просто напишите тест после обнаружения бага.
      6(1). Уже отписали выше — либо неудачная архитектура, либо неправильный подход к тестам. Хотя, при приемочных тестах да, приходится иногда много чего сгенерировать.
      6(2). Что значит «нужны»? В одной из статей одного из TDD-отцов было сказано примерно следующее: покрывайте код настолько мало, насколько позволяет ваша уверенность. Конечно, слепо следовать этому не стоит, но нужно иметь это ввиду. Например, я никогда не пишу тесты для контроллеров. Я считаю это пустой тратой времени, т.к. эти же случаи можно более наглядно протестировать в приемочных тестах. Я пишу тесты контроллеров только для специфических случаев. Кому-то это покажется неправильным (тут есть свои недостатки, согласен), но я делаю так.
      7. И опять это не проблема подхода TDD как такового.

      Многие воспринимают тесты как панацею. Мол, напишу я тесты и все будет тип-топ, код станет стабильным, безбажным, красивым и вообще. Это не так. Тесты помогают ускорить дальнейшую работу с кодом. Я трачу 10 минут на написание теста и экономлю часы в будущем для проверки этих же кейсов вручную. Тесты помогают сохранить работоспособность при рефакторинге. Написал функцию, запустил тесты — все в шоколаде, ничего нигде не упало, можно спать спокойно. Тесты помогают продумать архитектуру. Часто после написания тестов я просто создаю класс/функцию и сразу пишу «зеленый» код: все, что мне нужно было придумать и учесть, я придумал и учел во время написания тестов.

      Конечно, тут нужна сноровка. В некоторых специфических случаях я трачу немало времени, чтобы написать хороший тест. В дальнейшем для подобных моментов у меня уже будет решение и я буду писать по накатанному пути. Но когда я только начинал, тесты отнимали реально много времени в основном из-за незнания многих принципов. Слава богу, заказчик был не против.
    • 0
      2. изменения кода влекут переписывание многих (а то и всех) тестов.

      Налицо что TDD отсутствует в проекте. Изменение существующих или добавление новых тестов должно влечь изменение кода, а не наоборот.
      3. мнообразие тестов (скажем, форма из N полей, сводится к 2^N тестам), 3\4 из которых очевидна (как только начинаешь писать тест, уже видишь, код содержит баг или нет).

      Не знаю о каких формах речь, но 2^N тестов на форму из N полей мне сложно представить. Обычно линейная зависимость.
      4. тесты на специфичные баги: нашли баг, добавили тест, исправили, тест проходит, отлично; но погодите, а в других местах нет ли этого же бага?

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

      Если не задумываемся, то и в коде эта ситуация отражена не будет. При написании тестов (раньше кода) не нужно думать о всех возможных ситуациях больше чем при написании только кода (если не Фобос-Грунт разрабатываем). Покрываем основной сценарий, покрываем очевидные отказы (которые бы в коде проверили) и останавливаемся. Если что, то будет баг-репорт и пофиксим.
      6. сложность эмуляции окружения сложного (а особенно сильносвязанного) приложения: нужно создать mock-БД, наполнить верными и заведомо неверными данными, поднять фреймворк или эмулировать его часть

      Нужно создать стаб-объект, эмулирующий БД. Зачем нам настоящая? Баги в БД ловим? И т. д.
      6. многообразие типов тестов: не юнит-тестами едиными, нужны и интеграционные тесты, и автоматизированные тесты…

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

      А они не для этого пишутся. Да и тестами такие ошибки проще поймать.
      8. концепция TDD требует тщательного проектирования: если мы тесты пишем до написания кода, а детали реализации не видны до рабочего прототипа, то у нас проблемы.

      Ровно наоборот. Пишем тесты на основной сценарий (assert(4, sum(2,2));), хардкодим реализацию (sum (a, b) { return 4; }) и забиваем, пока приложение не выстроится из таких кусков.
      • 0
        Нужно создать стаб-объект, эмулирующий БД. Зачем нам настоящая? Баги в БД ловим? И т. д.

        Я как раз этот момент не могу понять. Вижу, что принято эмулировать все внешние зависимости (db, filesystem, http), так а что я, в таком случае, тестирую? Ну т.е. есть какой-то метод $obj->createSomething($data);, который создает что-то из полученных данных. Пусть он создает запись в БД. Если я внедряю фейковую базу, что я протестировал? Ну вернул он true, как и требовалось, но на реальных данных валится.

        Возможно, я сумбурно изложил мысль, но, пожалуйста, объясните мне про моки/стабы.
        • 0
          Моки-стабы используются для модульного (юнит) тестирования. В рамках тестирования $obj метод createSomething действительно ничего не делает, только возвращает true или false — первый кандидат на замену стабом для остальных методов. Сам createSomething видимо обращается к какой-то библиотеке, работающей с БД, чтобы проверить его, нужно эту библиотеку заменить на мок. То есть фэйковая библиотека будет проверять, что, например, сreateSomething вызывает $this->db->table('table')->insert($data) или, например, mysql_query(«INSERT INTO table ...»); (но тут сложнее вместо встроенной функцией подставить мок, хотя и возможно)
        • 0
          Ну вернул он true, как и требовалось, но на реальных данных валится.


          Ну почему же сразу true. В фейковых объектах можно делать много интересных проверок.
        • +2
          Ну вернул он true, как и требовалось, но на реальных данных валится.

          Значит проблема в реальных данных. Их и проверяйте. Например, взять Rails. Мне не нужно проверять в большинстве случаев, как работает ActiveRecord — это давно проверили за меня разработчики. Поэтому я могу мокнуть нужный мне AR-объект или класс и проверять обращения к нему с возвратом фейковых данных.
          Если не эмулировать зависимости, тесты станут очень чувствительными и начнут падать буквально от ошибки в любом методе. Да и вам же самому будет сложнее отловить ошибку. Представьте, что у вас ошибка в некоем методе X, который используется в пяти других методах, которые в свою очередь используются еще в двадцати методах итд. Если не стабить этот метод X в тестах, то при ошибке в нем мы получим груду свалившихся тестов, которые придется разгребать. В случае, когда метод застаблен, ошибка в нем никак не повлияет на тесты остальных методов, и вы получите в лучшем случае один упавший тест, а в худшем — несколько, но указывающих непосредственно на этот метод. Ошибку легко сразу локализовать и исправить.
          • 0
            Спасибо.
            • 0
              Я немного дополню — при этом нельзя забывать о том, что TDD — это не только юнит-тесты. Вы выбираете тот уровень тестов, который Вам прямо сейчас наиболее удобен. Если Вы находитесь в самом начале разработки и вообще без понятия, с чего взяться, первым тестом будет, скорее всего, приемочный тест для какой-то простой фичи типа отображения сообщения об ошибке. Если же Вы уже имеете конкретных юнитов в дизайне и прорабатываете их, то, как Вам сказали, чем более они будут изолированы от других юнитов (особенно инфраструктурных) тем лучше.
  • 0
    То есть TDD — это Test Driven Design? Хотя в статье дизайн настойчиво называется архитектурой.

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

    Честно говоря, мне кажется, что TDD был ответом на экспоненциальный рост количества программистов на рынке. Количества но не качества. Т.е. хороший инструмент, если в подчинении оказалась орда code monkeys.
    • 0
      Разработка через тестирование (англ. test-driven development, TDD) — техника разработки программного обеспечения, которая основывается на повторении очень коротких циклов разработки: сначала пишется тест, покрывающий желаемое изменение, затем пишется код, который позволит пройти тест, и под конец проводится рефакторинг нового кода к соответствующим стандартам. (wiki)
      Дело в том, что через тесты описывается архитектура приложения, а потом реализуется в коде… Хорошо написанные тесты заменяют собой документацию по архитектуре приложения.
    • 0
      нужны по возможности чёткие спецификации, потом хорошая модульная архитектура, затем слабо связанный дизайн

      расскажите подробнее, что это по-вашему и в чем между ними разница?
      • 0
        на примере.

        спецификация — это перечень пожеланий клиента. обычно очень абстрактно в виде «хочу апп, чтоб такси вызывать»
        архитектура — это перечень решений, как эти пожелания будут реализованы, но всё ещё абстрактно. «будет апп для платформ Х, У и Z, поставим сервак с REST интерфейсом и прочая. REST должен уметь то и сё, апп имеет следующие функции.»
        дизайн — это набор решений какие библиотеки, классы и паттерны использовать. Так как уже определено, что должен делать сервак/апп — можно говорить о модели классов и их общей функциональности. При этом ещё ни строчки кода не написано.
    • 0
      Т.е. хороший инструмент, если в подчинении оказалась орда code monkeys.

      нельзя ли объяснить, что это значит? TDD требует крайне высокой квалификации, и применение ТDD code monkeys — попросту невозможно.
      • 0
        Сам пишешь тесты, а реализацию отдаешь кодерам?
        • 0
          Это как? Вкратце: в TDD пишется один очередной тест, быстро реализуется, затем производится рефакторинг, в т.ч. высокоуровневый. Процесс реализации дает большое кол-во обратной связи и дает информацию для нахождения нового теста. Например, можно понять, что нужен еще один класс, ибо исходный берет на себя слишком много. Рефакторинг — тема вообще отдельного разговора.
          • 0
            Пишешь тест, отдаешь на реализацию, а сам пока рефакторишь предыдущую итерацию
            • 0
              Реализация теста — это 2-3 минуты, и основной бенефит от нее — понять, удалось ли влезть в эти рамки, и если не удалось — искать другой тест. Как это можно делегировать?
              • 0
                Это смотря какого теста.
                • 0
                  Это смотря какого теста

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

                  Этой теме, кстати, тоже традиционно уделяется мало внимания. Я говорю о ритме, который играет важную роль в процессе. Подробности см. у Кента Бека.
                  • 0
                    при этом нельзя забывать о том, что TDD — это не только юнит-тесты. Вы выбираете тот уровень тестов, который Вам прямо сейчас наиболее удобен.

                    :)
      • 0
        Пирмер 1. Реальный.

        Моя текущая контора наняла нового тим лида. крайне некомпетентный в разработке. зато профессиональный бюрократ и защитник своей задницы. через месяц-два его работы стало понятно, что его интересует исключительно правильная отчётность. для него ТДД — идеальный инструмент, потому что он сразу может рапортовать, что покрытие кода 100% и ошибок в нём «просто не может быть»™. Реальное качество кода его не интересует. Для него кодеры — зоопарк в котором приходится работать потому что он любит деньги. К счастью у нас уже давно оговорено покрытие не меньше 75% как прагматичный критерий.

        Пример 2. Выдуманый.

        Вы — тех лид и начальство приходит с радостной новостью — вам выделена команда. Правда в индии. Правда в конторе про которую никто не слышал. Но зато вот команда и вот срок когда продукт нужно сдать.

        В этой ситуации нужно либо написать такую подробную спецификацию, что, в принципе, она не будет отличаться от готовой программы, либо организовать с командой ТДД. Во-первых она вынуждает хотя бы слабую связанность — иначе код невозможно тестировать, во-вторых появляется вторая «перспектива» для ревью и, как следствие, шанс быть уверенным, что пишется то, что нужно. В этом случае ТДД — это зеркало спецификации.
        • 0
          В этом случае ТДД — это зеркало спецификации.

          Причем, как правило, не допускающее неоднозначных толкований, «потом сделаю» и т. п.
        • 0
          Я, честно говоря, не совсем понял, о чем мы говорим :) Одно могу сказать точно: если тимлид- дурак, или разработчики — дураки, то ни TDD, ни что-либо другое не поможет создать нормальное приложение. Нет на сей день методики, которая позволяет программировать, не включая мозг.
          • 0
            мы оживлённо и бурно друг с другом соглашаемся ;)
  • 0
    Почему бы перед тем, как писать класс, не подумать, как он будет использоваться? То есть, разработать типичные сценарии вызовов. А от этого и до тестов недалеко.

    По себе заметил, что тесты вперед писать получается редко. Но также заметил: если пишешь вперед реализацию, получаются классы, которые проще реализовать. Когда пишешь вначале тесты получаются классы, которыми легче пользоваться.
  • 0
    TDD — это не серебряная пуля, это просто техника. Где-то она полезна, где-то нет. Как раз профессинализм разработчика помогает определить, где нужно сделать быстро, а где качественно (с TDD или нет, вопрос уже не такой важный).
  • 0
    Лично мне очень понравилась книжка Кент Бек «Экстремальное программирование. Разработка через тестирование»
    Не скажу, что познал и применяю TDD в полном объеме, но после прочтения книжки начал применять TDD сразу в рабочем проекте(проект, правда, не большой(порядка 3 тысяч строк на данный момент), и вся архитектура была тщательно продумана изначально
    • 0
      Кент Бек — это классика. На меня она тоже сильно повляла. Правда не сразу. Действовала, как вирус.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.