Прагматичность TDD

Original author: Robert C. Martin
  • Translation
Итак, моя последняя запись: стартап-ловушка (здесь её перевод — прим. переводчика) наделала много шуму. Среди людей, выражающих согласие и поддержку, нашлась и группа людей, которая была категорически не согласна. Я не буду здесь резюмировать все разногласия, ибо в этом месяце я уже исчерпал свой лимит ругательных слов. Но одним альтернативным мнением я проникся и считаю нужным его обсудить.
Речь о старом конфликте «прагматизм против догматизма». Недовольство заключалось главным образом в том, что я проявил себя слишком догматичным. Альтернативное мнение заключается в том, что в одних случаях применение TDD может быть оправдано, а в других TDD может вылиться в слишком высокие затраты. Так что вы должны быть прагматиком и делать выбор с умом.
На первый взгляд, эта идея звучит совершенно обоснованно. В конце концов, прагматизм это же хорошо, не так ли? Предположим, что вы знали бы заранее, что, применяя TDD, вы не уложитесь в срок, и, что, если бы не применяли, то уложились бы, вы бы не стали применять TDD, верно?
Верно. Без вопросов. И, действительно, иногда так бывает, что такой путь является правильным. Цель этого поста в том, чтобы пояснить то, когда же, по моему мнению, применение TDD может быть слишком затратным.
Но перед тем как я начну, я хотел бы высказать кое-какое соображение. TDD – это дисциплина для программистов, такая же как ведение бухгалтерии по методу двойной записи для бухгалтеров, или процедура стерилизации инструментов для хирургов. Бывают ли случаи, когда бухгалтеры не применяют метод двойной записи? Бывают ли случаи, когда хирурги не стерилизуют инструменты?
Ответ – да, в обоих случаях. Я сомневаюсь даже в том, что бухгалтеры применяют метод двойной записи при сведении баланса своих личных чековых книжек, или когда сверяют итоговую сумму по счёту в ресторане. Я мог бы быть признан неправым насчёт первого утверждения, в конце концов, я сам применял метод двойной записи годами при сведении баланса моей чековой книжки. Но я постепенно понял, что затрачиваемые усилия не покрывают возможные риски. Что касается последнего примера, то я думаю, все согласятся, что для проверки счёта в ресторане, метод двойной записи – это явный перебор.
Теперь что касается хирургов и процедуры стерилизации: пару лет назад мне удаляли липому с ноги. Моя жена наблюдала за этой процедурой. Операция была проведена под местным наркозом в кабинете у врача. Я слышал, что жена спросила врача о том, почему он не провёл процедуру стерилизации в процессе подготовке к операции. Он ответил, что для такой простой операции достаточно «процедуры очистки». Мы с женой удовлетворились этим ответом, и врач провел операцию.
Спустя пару дней, разрез воспалился и начал болеть. В один из швов попала инфекция и врачам пришлось заново сделать разрез и всё там почистить. Я не знаю, была ли причиной «процедура очистки», но с тех пор я настаиваю на том, чтобы лечащий меня врач, проводил процедуру стерилизации, а не «процедуру очистки».
Однако, утверждение остаётся верным. Бывают случаи, когда TDD слишком затратна и следует применять более лёгкие дисциплины. Я надеюсь, что моя история убедила вас в том, что такие случаи крайне редки и, что мем о прагматизме не должен стать помехой для применения ваших дисциплин просто потому что они кажутся неподходящими.

Прагматичность.

Итак, когда я не применяю TDD?
  • Я не пишу тесты для свойств get и set. Обычно, написание таких тестов – глупая практика. Эти самые get и set будут протестированы косвенно через тесты остальных методов. Таким образом, непосредственное тестирование get и set бессмысленно.
  • Я не пишу тесты для переменных-членов. Они также будут косвенным образом протестированы.
  • Я не пишу тесты для функций в одну строку или для функций которые абсолютно тривиальны. Опять же, они будут протестированы косвенным образом.
  • Я не пишу тесты для графического интерфейса. Тестирование GUI подразумевает мороку. Что-то нужно сдвинуть на место, посредством изменения размера шрифта там, значения RGB сям, XY-координат вон тут, ширины поля вон там. Применять таким образом TDD глупо и расточительно.
  • Однако, я убеждаюсь в том, что любая значительная обработка будет перемещена из GUI-кода в модули пригодные для тестирования. Я не позволяю важным кускам кода оставаться не протестированными. Таким образом, мой GUI-код представляет собой не многим более, чем просто клей и проводочки, которые вытягивают данные и показывают их на экране (см. статьи по MVVM или MVP)


  • В целом я не пишу тесты для кода, над которым я должен «химичить» методом проб и ошибок. Но я отделяю такое «химичинье» от кода в котором я уверен
  • Иногда, я, похимичив немного с кодом, затем пишу на него тесты.
  • Также, иногда, я удаляю код с которым уже «похимичил» и заново переписываю его в манере TDD.
  • Как вам поступать в таких ситуациях – дело вкуса.


  • Пару месяцев назад я написал программу в 100 строк длиной без каких-либо тестов (открываем рты от изумления!)
  • Программа была одноразовой. Она была использована один раз и затем выброшена. (Она была предназначена для создания спецэффекта в одном из моих видео).
  • Программа была длинной в один экран. По сути это было чистое GUI-приложение. Так что я просто похимичил со всем что мне было нужно в одном месте.
  • Я написал её на Clojure, и у меня была простая интерактивная среда программирования. Я могу запустить разрастающуюся программу из этой простой интерактивной среды и увидеть результаты исполнения каждой строки кода, которые я только что написал. Это не был TDD, это был EDD (Eye Driven Development – разработка через зрительное слежение)


  • Обычно я не пишу тесты для фрэймворков, баз данных, веб-серверов или другого ПО третьей стороны. Я пишу для них моки и тестирую свой код, а не их.
  • Конечно, иногда я тестирую код третьей стороны. Тестирую если:
  • Есть ощущение что он поломан
  • Я получаю настолько быстрые и предсказуемые результаты, что написание мока будет избыточным.


Всё это не догма.

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

От переводчика: Марк Симэн уже прочитал эту статью дяди Боба и отреагировал своей, в которой не соглашается с некоторыми моментами и останавливается на них чуть подробнее. Будет время — постараюсь перевести.

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 43

    +6
    Я не пишу тесты для функции в одну строку или для функций который абсолютно тривиальны. Опять же, они будут протестированы косвенным образом.

    Чревато не заметить переход функции от тривиальной к сложной. Вроде функция тривиальна, сделали тривиальное изменение, и вот она уже не тривиальна и косвенно не факт, что получится проверить. Лучше, по-моему, подсуетиться и написать триальный тест сразу.

    Аналогично с сеттерами и геттерами. Простейший тест $obj->setProperty(1); $this->assertEquals(1, $obj->getProperty()); написать не сложно (причем он тестирует сразу и геттер и сеттер), зато не забудешь сделать тест, когда решишь, например, в сеттере логирование сделать или исключения бросать для невалидных значений.
      0
      Я тоже считаю, что в написании тестов для геттеров и сеттеров есть смысл, ибо тесты — это документация. Тот тривиальный тест, который я сейчас напишу на геттер или сеттер явным образом укажет, что именно такое поведение ожидается от этих геттеров и сеттеров. Если не написать такой\такие тест\тесты, то пришедший на смену меня программист изменит их поведение, усложнив их, и тесты могут и не провалиться (которые, якобы, должны их косвенно тестировать).
        +2
        В TDD прежде, чем изменять что-то, у вас должен быть сломанный тест и ситуаций «могут и не провалиться» не должно быть.
          +1
          А если на геттер вы не напишите теста? Теста нет. Откуда вы знаете, что следующий разраб будет писать тест на этот геттер, прежде чем изменит его? Другой разраб может вообще не практиковать TDD или не любить его, но лично моя задача быть профессионалом и передать код хорошего качества, который не может быть поломан на раз каким-нибудь нерадивым программистом. Мои тесты хотя бы должны будут ему указать на то, что не всё так просто как ему кажется.
        +5
        >>подсуетиться и написать триальный тест сразу.
        Чтобы утонуть в громадном кол-ве тривиальных вещей? ;) Задача разработчика не выдать абсолютно безбажный код, его задача в другом в достаточно быстрой правке кода. Если тестировщик нашел багу, он может определить место и если нет юнит-теста, то написать его. Но думать о каждом месте кода: «А вдруг здесь будет бага?» это утопия.

        >>Простейший тест $obj->setProperty(1); $this->assertEquals(1, $obj->getProperty())
        На мой взгляд геттеры и сеттеры не нужно тестировать. Попробую пояснить мысль.
        Разрабатывая продукт объектно ориентированно мы пишем классы, но что такое «классы» мы часто забываем. Прошу простить что напоминаю. Классы это «абстрактный тип данных, т.е. набор данных и действий с ними». Если мы пишем функцию в виде:

        def my_super_cool_action():
          value1 = obj.get_value1()
          value2 = obj.get_value2()
          ...
          valueN = obj.get_valueN()
        # делаем что-нибудь с этими value1,...,valueN
        


        Если мы написали такой код, то мы написали его не в ОО-виде! Мы неправильно написали метод, мы должны переместить метод в область класса. После этого перемещения мы можем написать более простой тест-код:

        def test_my_feature():
           obj.my_super_cool_action()
        


        Более того необходимость в геттерах отпадает и тестировать их не нужно.
          0
          К сожалению, геттеры/сеттеры используются о OR-механизмах представления состояния объектов в СУБД, например. Валидность переданных значений в сеттерах можно поймать на объектном уровне, а не на уровне СУБД, использую ассерты.
            0
            Ну геттеры\сеттеры тоже бывают разные:
            Вариант №1:
            def get_value(self):
               return self._internal_field
            


            Вариант №2:
            def get_value(self):
               external_api_method1( constant1 )
               return external_api_method2()
            


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

                  Нет, это точная локализация регресии. При косвенном тесте мы точно не можем знать что сломалось — сеттер или тестируемый метод. А если вместе с «главным» упавшим тестом, упадет и тест на акцессоры, то проблема ясна.
                    0
                    >> При косвенном тесте мы точно не можем знать что сломалось
                    Точность не нужна! Нам нужна, как правило, более менее качественная разработка за приемлемое время! Толку то от этих тестов ступид-геттеров, если это же время можно потрать на интеграционный тест?
                      0
                      Они делаются за секунды. А сэкономить могут часы.
                        0
                        Хорошо. Давайте не будем спорить, а поверим практике.
                        Приведите 2-3 примера из Вашей реальной жизни те случаи когда такой юнит-тест, реально помог Вам! Причем не за всю вашу многолетнюю практику программирования, а за недалекое прошлое. Сможете привести такие примеры?
                          0
                          На днях копипастнул геттер и сделал опечатку перебивая имя переменной.
                            0
                            А Вам компилятор ничего не сказал? Еще, возможно, у Вас в классе было очень много однотипных переменных, что говорит о необходимости вынесения их в один общий класс или структуру. Чтобы не гадать «на кофейной гуще» лучше с подробностями.
                              0
                              Интерпретируемый язык со слабой динамической типизацией (PHP). Вместо $this->adress написал $this->adres
                                0
                                Ну я недавно тоже подобную оплошность, допустил ))) программируя на Python:
                                Подробнее
                                было:
                                	def stderr(self):
                                		self._toolResult['stderr']
                                

                                надо было:
                                	def stderr(self):
                                		return self._toolResult['stderr']
                                

                                Это привело к тому, что мой небольшой тест-скрипт тестирующий консольное приложение неправильно проверял stderr от приложения.

                                Как вывод написал скрипт на питоне же, чтобы проверял меня на наличие таких идиотских ошибок. Но такие ошибки не относятся к геттерам с тривиальным кодом! Я поступил проще, я написал скрипт который проверяет меня на наличие подобных ошибок. Что достиг этим:
                                1) Выловил еще пару ошибок в других моих скриптах
                                2) Не написал лишнего и избыточного юнит-теста
                                Подобные ошибки: ваша и моя, это «общескриптовое» это так сказать «систематическая ошибка рук разработчика» и ее лучше выделять для ВСЕХ ваших работ, а не только для конкретно одной!
                                  0
                                  Ещё параллельно на Ruby пишите? :)

                                    0
                                    Мои основные языки это C++(C++03), Python(3.x), Assembler(вставки или небольшие программы с помощью fasm). Ооооочееень лениво посматриваю в сторону C# и Go, но пока мне этих 3х языков вполне хватает. Ruby не впечатлил, его нишу в моем мозгу уверенно занимает Python
                                      0
                                      У меня почему-то с python не ассоциируется возврат из функции значения последнего выражения, а вот с ruby однозначно. А ruby сильно впечатлил, а python для меня язык шелл-скриптов.
                                        0
                                        >>А ruby сильно впечатлил, а python для меня язык шелл-скриптов.
                                        Ну простота написания кода и есть один из плюсов для питона. Если почитаете дзен питона, то там так и сказано «чем проще тем лучше» )
                                          0
                                          Читал. иСпользую даже в PHP. Хотя я не согласен с определениями «проще» в определениях синтаксиса.
            0
            Чтобы утонуть в громадном кол-ве тривиальных вещей? ;)

            Если не тонем в коде самого класса от акцессоров, то не утонем и коде теста :)
            На мой взгляд геттеры и сеттеры не нужно тестировать.

            Вы пояснили почему надобности в акцессорах нет в некоторых случаях. Если нет, то и тесты не нужны. Но я как-то привык писать акцессоры только когда в них есть надобность, а тогда и тесты бы неплохо написать.
          +8
          >>EDD (Eye Driven Development – разработка через зрительное слежение)
          По-моему это самый популярный вид программирования. )
            0
            Насчет тестирования GUI — бывают случаи, когда там «оказывается» код, связанный с синхронизацией движка и интерфейса и так далее, поэтому случается, что тестирование движка не выявляет все проблемы и приходится-таки писать тесты для GUI
              0
              Вот так читаешь перевод и только в конце понимаешь что автор статьи Роберт Мартин — мировой консультант в области разработки ПО.
                +1
                Рядом с заголовком статьи значится флаг «перевод».
                +1
                Реквестирую продолжение. Очень хорошее дело делаете
                  0
                  Спасибо большое. Я постараюсь.
                • UFO just landed and posted this here
                    0
                    Вы уверены, что список правильно отформатирован?
                      0
                      Уверен, что неправильно. Напишите, пожалуйста, в личку как исправить положение.
                      0
                      Суперский троллинг прошлой статьи был адски успешен, и Боб Мартин пытается повторить свой успех :)
                      Или же сказать, что он-де не троллил, а реально за все хорошее и против всего плохого.
                        +1
                        Подкинте статейку\книгу по тестированию и TDD. С++.
                          0
                          Я вот, читая все это обсуждение про тривиальные и не очень геттеры/сеттеры, внезапно поймал себя на мысли о том, что последний раз делал тривиальное свойство (в терминах C#) очень давно.

                          Либо это был DTO/модель данных (и там тривиальные свойства объекта юнит-тестами покрывать бессмысленно, потому что они покрываются интеграционными с БД/сервисами), либо это нетривиальное свойство (хотя бы расчетное, а то и с более сложной логикой), и тогда его надо покрывать тестами с самого начала.
                            0
                            Ну, у меня в проектах часто встречаются тривиальные свойства с public get и private set. Это очень удобно.
                              0
                              Если public get/private set, то оно уже не тривиальное, потому что логика сложнее, чем у публичного поля (и, раз уж значение свойства назначается где-то внутри, то на это «внутри» должен быть тест).
                                0
                                Здесь мы с вами как раз втыкаемся в проблему о которой сказал Марк Симэн в своём «обзоре» этого поста дяди Боба: «что есть тривиальный?»
                                В целом, я с вами согласен.
                                  0
                                  Я в своем комменте имел в виду вполне конкретное public int Count {get; set;}.
                            0
                            Я не любитель TDD, т.к. обычно сразу представляю себе как должен выглядеть код, сразу его пишу, сразу рефакторинг, после – тесты.
                            Но несколько раз возникали ситуации, сложные задачи, когда я абсолютно не представлял себе каким образом эта штука должна работать, где и что я должен поменять, чтобы добиться верного результата во всех случаях. Я написал предварительно несколько интеграционных и модульных тестов, после чего шаг за шагом исправлял баги.
                            Т.е. для себя вижу пользу TDD в том, что я смогу написать и изменить тот код (при этом качественно и быстро), который бы не смог написать без предварительных тестов (или это было бы чрезвычайно сложно, в несколько этапов проб, ошибок, возвратов к исходному).
                              0
                              Это не TDD. Это лишь его часть в виде Test-First.
                                0
                                Написал тест-фейл-написал код-успех-повтор. Потом рефакторинг. Использование factories, fixtures, kiss, integrational tests и прочее могут применяться независимо от термина TDD. Прочитав вики, я не увидел существенной разницы TDD с тем, о чем я написал. Поясните, пожалуйста?
                                  0
                                  TDD больше о последовательном изменении тестов с соответствующем изменением кода.  «test»->«simplest implementation»->«refactoring»->«new test»->…
                                  TFD больше о цели. «acceptance test»->«development until test passed»->«refactoring»
                                  Технически они похожи, но отвечают на разные вопросы. В TDD код для тестов, в TFD — тесты для кода.

                            Only users with full accounts can post comments. Log in, please.