32. Развитие навыков TDD
В данной главе я намерен сформулировать несколько вопросов, над которыми полезно подумать, если вы намерены интегрировать TDD в свой процесс разработки. Некоторые из вопросов просты, другие требуют тщательного обдумывания. Ответы на некоторые из этих вопросов вы найдете здесь же, однако иногда, чтобы ответить на некоторый вопрос, вам придется провести собственные исследования.
Насколько большими должны быть шаги?
В этом вопросе на самом деле скрыто два вопроса:
• Какой объем функциональности должен охватывать каждый тест?
• Как много промежуточных стадий должно быть преодолено в процессе каждого сеанса рефакторинга?
Вы можете писать тесты так, что каждый из них будет требовать добавления в функциональный код единственной строки и выполнения небольшого рефакторинга. Вы также можете писать тесты так, что каждый из них будет требовать добавления в функциональный код сотен строк, при этом у вас будут уходить часы на рефакторинг. Какой из этих путей лучше?
Часть ответа состоит в том, что вы должны уметь работать и так и этак. Общая тенденция TDD очевидна – чем меньше шаги, тем лучше. В данной книге мы занимались разработкой маленьких тестов на уровне отдельных фрагментов программы. Однако некоторые программисты экспериментируют в области разработки программ, исходя из тестов на уровне всего приложения.
Если вы только приступаете к освоению рефакторинга, вы должны двигаться маленькими шажками. Процесс ручного рефакторинга чреват ошибками. Чем больше ошибок вы наделаете, тем меньшим будет ваше желание выполнять рефакторинг в дальнейшем. После того как вы 20 раз проделаете рефакторинг малюсенькими шажками, можете приступать к экспериментам по удлинению этих шажков.
Автоматизация существенно ускоряет процессы, связанные с рефакторингом. То, что раньше требовало выполнения 20 маленьких шагов вручную, теперь становится единственным пунктом в меню. Количество изменений, выполняемых в ходе сеанса рефакторинга увеличивается на порядок, и это неизменно сказывается на качестве. Когда вы знаете, что в вашем распоряжении великолепный инструмент, вы становитесь более агрессивным при выполнении рефакторинга. Вы пытаетесь ставить больше экспериментов и смотреть, какой способ структурирования кода лучше.
На момент написания данной книги браузер рефакторинга Refactoring Browser for Smalltalk по-прежнему является наилучшим инструментом в этой категории. В настоящее время многие среды разработки для Java поддерживают развитые средства рефакторинга. Кроме того, поддержка рефакторинга появилась и в других языках и средах разработки.
Что не подлежит тестированию?
Флип предложил высказывание, которое может служить ответом на этот вопрос: «Пишите тесты до тех пор, пока страх не превратится в скуку». Высказывание подразумевает, что вы должны найти ответ сами. Однако вы читаете эту книгу для того, чтобы найти в ней ответы на вопросы, поэтому попробуйте воспользоваться следующим списком. Тестировать следует:
• условные операторы;
• циклы;
• операции;
• полиморфизм.
Однако только те из них, которые вы написали сами. Не тестируйте чужой код, если только у вас нет причин не доверять ему. В некоторых ситуациях недостатки (можно сказать жестче: «ошибки») во внешнем коде заставляют добавлять дополнительную логику в разрабатываемый вами код. Надо ли тестировать подобное поведение внешнего кода? Иногда я документирую непредсказуемое поведение (ошибку) внешнего кода при помощи теста, который перестанет выполняться, если в следующей версии внешнего кода ошибка будет исправлена.
Как определить качество тестов?
Тесты – это канарейка, которую берут в угольную шахту, чтобы по ее поведению определить присутствие запаха плохого дизайна. Далее перечисляются некоторые атрибуты тестов, которые указывают на то, что дизайн тестируемого кода начинает плохо пахнуть:
• Длинный код инициализации. Если вы вынуждены написать сотни строк кода, создавая объекты для одного простого оператора assert(), значит, что-то не так, значит, ваши объекты слишком большие и их требуется разделить.
• Дублирование кода инициализации. Если вы не можете быстро найти общее место для общего кода инициализации, значит, у вас слишком много объектов, которые слишком тесно взаимодействуют друг с другом.
• Тесты выполняются слишком медленно. Если тесты TDD работают слишком медленно, значит, они не будут запускаться достаточно часто. Значит, программист будет в течение некоторого времени работать, вообще не запуская тестов. Значит, когда он их все-таки запустит, скорее всего, многие из них не сработают. На самом деле здесь кроется серьезная проблема: если тесты работают медленно, значит, тестирование частей и компонентов разрабатываемого приложения связано с проблемами. Сложности при тестировании частей и фрагментов приложения указывают на существование недостатков дизайна. Иными словами, улучшив дизайн, вы можете увеличить скорость работы тестов. (Продолжительность работы набора тестов не должна превышать десяти минут, по аналогии с ускорением свободного падения в 9,8 м/с2. Если для выполнения набора тестов требуется более 10 минут, этот набор обязательно надо сократить или тестируемое приложение должно быть оптимизировано так, чтобы для выполнения набора тестов требовалось не более 10 минут.)
• Хрупкие тесты. Если ваши тесты неожиданно начинают ломаться в самых непредсказуемых местах, это означает, что одна часть разрабатываемого приложения непредсказуемым образом влияет на работу другой части. В этом случае необходимо улучшить дизайн так, чтобы данный эффект исчез. Для этого можно либо устранить связь между частями приложения, либо объединить две части воедино.
Как TDD способствует созданию инфраструктур?
Инфраструктура (framework) – набор обобщенного кода, который можно использовать в качестве базы при разработке разнообразных прикладных программ. На самом деле TDD является неплохим инструментом разработки инфраструктур. Парадокс: если вы перестаете думать о будущем вашего кода, вы делаете код значительно более адаптируемым для повторного использования в будущем.
Очень многие умные книги говорят об обратном: «кодируйте для сегодняшнего дня, но проектируйте для завтрашнего» (code for today, design for tomorrow). Похоже, что TDD переворачивает этот совет с ног на голову: «кодируйте для завтрашнего дня, проектируйте для сегодняшнего» (code for tomorrow, design for today). Вот что происходит на практике:
• В программу добавляется первая функциональность. Она реализуется просто и прямолинейно, поэтому реализация выполняется быстро и с наименьшим количеством дефектов.
• В программу добавляется вторая функциональность, которая является вариацией первой. Дублирование между двумя функциональностями объединяется и размещается в одном месте. Различия оказываются в разных местах (как правило, в разных методах или в разных классах).
• В программу добавляется третья функциональность, которая является вариацией первых двух. Уже имеющаяся общая логика, как правило, может использоваться в том виде, в котором она уже присутствует в программе, возможно, потребуется внести незначительные изменения. Отличающаяся логика должна располагаться в отдельном месте – в другом методе или в другом классе.
В процессе разработки мы постепенно приводим код в соответствие с принципом открытости/закрытости (Open/Closed Principle), утверждающим, что объекты должны быть открыты для использования, но закрыты для модификации. Самое интересное, что при использовании TDD этот принцип выполняется именно для тех вариаций, с которыми действительно приходится иметь дело на практике. То есть TDD позволяет формировать инфраструктуры, удобные для представления таких вариаций, с необходимостью реализации которых программист сталкивается на практике. Однако эти инфраструктуры могут оказаться неэффективными в случае, если потребуется реализовать вариацию, которая редко встречается в реальности (или которая не была еще реализована ранее).
Что же произойдет, если необходимость реализации непредвиденной вариации возникнет спустя три года после разработки инфраструктуры? Дизайн быстро эволюционирует так, чтобы сделать вариацию возможной. Принцип открытости/закрытости на короткое время будет нарушен, однако это нарушение обойдется относительно недорого, так как имеющиеся тесты дадут вам уверенность в том, что, изменив код, вы ничего не поломаете.
В пределе, когда вариации возникают достаточно быстро, стиль TDD невозможно отличить от заблаговременного проектирования. Однажды я всего за несколько часов с нуля разработал инфраструктуру составления отчетов. Те, кто следил за этим, были абсолютно уверены, что это трюк. Они думали, что я сел за разработку, уже имея в голове готовую инфраструктуру. Однако это не так. Просто я долгое время практиковал TDD, благодаря этому я исправляю допущенные мною многочисленные ошибки быстрее, чем вы успеваете заметить, что я их допустил.
Сколько должно быть тестов?
Насколько емкой должна быть обратная связь? Рассмотрим простую задачу: дано три целых числа, обозначающих длины сторон треугольника. Метод должен возвращать:
• 1 – в случае, если треугольник равносторонний;
• 2 – в случае, если треугольник равнобедренный;
• 3 – в случае, если треугольник не равносторонний и не равнобедренный.
Если длины сторон заданы некорректно (невозможно построить треугольник со сторонами заданной длины), метод должен генерировать исключение.
Вперед! Попробуйте решить задачу (мое решение, написанное на языке Smalltalk, приведено в конце данного подраздела).
Это отчасти напоминает игру «Угадай мелодию» («Я могу закодировать задачу за четыре теста!» – «А я – за три!» – «О’кей попробуйте».) Для решения задачи я написал шесть тестов, а Боб Биндер в своей книге Testing Object-Oriented Systems («Тестирование объектно-ориентированных систем») для этой же самой задачи написал 65 тестов. Сколько на самом деле нужно тестов? Вы должны решить это сами, исходя из собственного опыта и рассуждений.
Когда я думаю о необходимом количестве тестов, я пытаюсь оценить приемлемое среднее время между сбоями (MTBF, Mean Time Between Failures). Например, в языке Smalltalk целые числа ведут себя как целые числа, а не как 32-битные значения. Иными словами, максимально возможное значение целого числа ограничивается не тридцатью двумя битами, а объемом памяти. Это означает, что вы можете обойтись без тестирования MAXINT. Безусловно, определенный предел существует, ведь теоретически можно создать целое число, для хранения которого не хватит имеющейся памяти. Но должен ли я тратить время на написание и реализацию теста, пытающегося заполнить память невероятно огромным целым числом? Как это повлияет на MTBF моей программы? Если я в обозримом будущем не собираюсь иметь дело с треугольниками, размер сторон которых измеряется такими числами, значит, моя программа не станет существенно менее надежной, если я не реализую такой тест.
Имеет ли смысл писать тот или иной тест? Это зависит от того, насколько аккуратно вы оцените MTBF. Если обстоятельства требуют, чтобы вы увеличили MTBF от 10 лет до 100 лет, значит, имеет смысл уделить время для разработки самых маловероятных и чрезвычайно редко возникающих ситуаций (если, конечно, вы не можете каким-либо иным образом доказать, что подобные ситуации никогда не могут возникнуть).
Взгляд на тестирование в рамках TDD прагматичен. В TDD тесты являются средством достижения цели. Целью является код, в корректности которого мы в достаточной степени уверены. Если знание особенностей реализации без какого-либо теста дает нам уверенность в том, что код работает правильно, мы не будем писать тест. Тестирование черного ящика (когда мы намеренно игнорируем реализацию) обладает рядом преимуществ. Если мы игнорируем код, мы наблюдаем другую систему ценностей: тесты сами по себе представляют для нас ценность. В некоторых ситуациях это вполне оправданный подход, однако он отличается от TDD.
TriangleTest
testEquilateral
self assert: (self evaluate: 2 side: 2 side: 2) = 1
testIsosceles
self assert: (self evaluate: 1 side: 2 side: 2) = 2
testScalene
self assert: (self evaluate: 2 side: 3 side: 4) = 3
testIrrational
[self evaluate: 1 side: 2 side: 3]
on: Exception
do: [: ex | ^self].
self fail
testNegative
[self evaluate: -1 side: 2 side: 2]
on: Exception
do: [: ex | ^self].
self fail
testStrings
[self evaluate: ‘a’ side: ‘b’ side: ‘c’]
on: Exception
do: [: ex | ^self].
self fail
evaluate: aNumber1 side: aNumber2 side: aNumber3
| sides |
sides:= SortedCollection
with: aNumber1
with: aNumber2
with: aNumber3.
sides first <= 0 ifTrue: [self fail].
(sides at: 1) + (sides at: 2) <= (sides at: 3) ifTrue: [self fail].
^sides asSet size
Когда следует удалять тесты?
Чем больше тестов, тем лучше, однако если два теста являются избыточными по отношению друг к другу, должны ли вы сохранить оба этих теста в наборе? Это зависит от двух критериев.
• Первый критерий – это уверенность. Никогда не удаляйте тест, если в результате этого снизится ваша уверенность в поведении системы.
• Второй критерий – это коммуникация. Если у вас есть два теста, которые тестируют один и тот же участок кода, однако читателем эти тесты рассматриваются как два различных сценария, сохраните оба теста.
Отсюда следует, что, если у вас есть два теста, которые можно считать избыточными как в отношении уверенности, так и в отношении коммуникации, удалите наименее полезный из этих тестов.
Как язык программирования и среда разработки влияют на TDD?
Попробуйте использовать подход TDD в среде Smalltalk с браузером Refactoring Browser. Теперь попробуйте работать в среде C++ с редактором vi. Почувствуйте разницу.
В языках программирования и средах разработки, в которых цикл TDD выполняется сложнее (тест – компиляция – запуск – рефакторинг), возникает тенденция двигаться вперед более длинными шагами:
• каждый тест охватывает больший объем кода;
• рефакторинг выполняется с меньшим количеством промежуточных шагов.
Приводит ли это к замедлению разработки, или, наоборот, разработка ускоряется?
В языках программирования и средах, в которых проще выполнить цикл TDD, у вас будет возникать желание больше экспериментировать с кодом. Позволит ли это двигаться быстрее и формировать лучшие решения, или вам кажется, что лучше тратить время на дополнительные размышления о дизайне?
Можно ли использовать TDD для разработки крупномасштабных систем?
Позволяет ли методика TDD разрабатывать крупномасштабные программные проекты? Какие новые типы тестов вам потребуется написать? Какие новые шаблоны рефакторинга могут потребоваться?
Самой крупной программной системой, целиком и полностью разработанной в стиле TDD, в создании которой я принимал участие, является система LifeWare (www.lifeware.ch). Работа над системой велась в течение 4 лет. Объем работ оценивается в 40 человеко-лет. На текущий момент система включает в себя 250 000 строк функционального и 250 000 строк тестирующего кода (на языке Smalltalk). Набор тестов системы включает в себя 4000 тестов, для выполнения которых требуется 20 минут. Полный набор тестов запускается несколько раз каждый день. Реализованный в системе огромный объем функциональности, похоже, никак не снижает эффективности TDD. Избавляясь от дублирования, вы стараетесь создать большое количество маленьких объектов, которые можно тестировать изолированно друг от друга вне зависимости от размера приложения.
Можно ли осуществлять разработку через тестирование на уровне приложения?
Если мы будем выполнять разработку, используя только внутренние программистские тесты (их называют тестами модулей – unit tests, – хотя они не вполне соответствуют этому определению), мы рискуем столкнуться с проблемой: полученная в результате этого система может оказаться не совсем тем или, что хуже, совсем не тем, что хочет получить пользователь. Программист будет работать над программой, которая, по его мнению, должна быть полезна, однако у пользователя может оказаться совершенно другое мнение. Чтобы решить проблему, можно разработать набор тестов на уровне приложения. Разработкой этих тестов должны заниматься сами пользователи (при поддержке программистов). Написанные пользователями тесты должны точно определять, что именно должна делать разрабатываемая система. Такой стиль можно назвать разработкой через тестирование на уровне приложения (ATDD, Application Test-Driven Development).
Встает техническая проблема: как написать и запустить тест для функциональности, которая еще не существует? Мне кажется, что всегда можно найти способ решения этой проблемы. Например, можно разработать интерпретатор, который будет вежливо сигнализировать о том, что обнаружен тест, выполнить который на данный момент невозможно по причине отсутствия в системе необходимых возможностей.
Существует также социальная проблема. У пользователей (на самом деле я имею в виду команду, в состав которой входят пользователи) появляется новая обязанность: разработка тестов. Процедура разработки тестов уровня приложения требует добавления дополнительного этапа в цикл работы над продуктом, – а именно, разработка пользовательских тестов выполняется перед началом реализации очередного объема функциональности. Организации часто сопротивляются подобному сдвигу ответственности. Новый этап требует координированных усилий множества членов команды, то есть перед тем, как приступить непосредственно к разработке кода, члены команды вынуждены потратить время на разработку пользовательских тестов.
Описанная в данной книге методика TDD целиком и полностью находится под вашим контролем. Иначе говоря, выполнение TDD зависит только от одного человека – от вас. Если у вас возникло желание, вы можете начать использовать ее с сегодняшнего дня. Однако если вы будете смешивать ритм красный – зеленый – рефакторинг с техническими, социальными и организационными проблемами разработки пользовательских тестов, вы вряд ли сможете добиться успеха. В данном случае следует воспользоваться правилом «Тест одного шага» (One Step Test). Сначала добейтесь равномерности ритма красный – зеленый – рефакторинг в собственной практике, затем расширьте область применения TDD.
Еще один аспект ATDD: определение длины цикла между разработкой теста и получением результатов его работы. Если заказчик написал тест, а потом в течение десяти дней ждет его срабатывания, это значит, что он большую часть времени смотрит на красную полосу. Если я работаю в стиле TDD на уровне программиста, я
• немедленно получаю зеленую полосу;
• упрощаю внутренний дизайн.
Как перейти к использованию TDD в середине работы над проектом?
У вас есть некоторый объем кода, про который можно сказать, что он корректно работает в большей или меньшей степени. Теперь вы хотите разрабатывать весь новый код в рамках концепции TDD. Что делать?
О проблеме перехода на использование TDD в середине работы над проектом можно написать целую книгу (или даже несколько книг). В данном небольшом разделе я очень поверхностно затрону несколько связанных с этим вопросов.
Самая большая проблема заключается в том, что код, изначально написанный без тестов, как правило, сложен в тестировании. Интерфейсы и взаимосвязи между объектами недостаточно хорошо спроектированы, поэтому сложно изолировать некоторый кусок логики, запустить его и проверить результаты.
«Надо это исправить», – скажете вы. Да, однако любой рефакторинг (без применения средств автоматизации), скорее всего, приведет к возникновению ошибок, и эти ошибки сложно будет обнаружить, так как у вас нет тестов. Проблема яйца и курицы. Змея кусает себя за хвост. Замкнутый цикл саморазрушения. Что же делать?
Прежде всего скажу, чего делать не надо: не надо писать тесты для всего кода и выполнять рефакторинг всего кода. Для этого может потребоваться несколько месяцев, в течение которых у вас не будет времени добавить в систему новой функциональности. Если вы тратите имеющиеся у вас деньги и при этом не зарабатываете новых, долго вы не протянете.
Поэтому прежде всего мы должны ограничить область планируемых нами изменений. Если мы видим часть системы, которую можно существенно улучшить, но которая вполне может быть оставлена без изменений на текущий момент, мы оставляем их без изменений. Возможно, взглянув на код и вспомнив грехи прошлого, вы не сможете удержаться от слез, однако возьмите себя в руки, – если код не требует немедленного вмешательства, лучше не изменять его.
Во-вторых, мы должны разорвать замкнутый круг между тестами и рефакторингом. Мы можем использовать в качестве обратной связи не только тесты, но и другие способы, например чрезвычайно осторожная работа в паре с партнером. Мы можем использовать обратную связь более высокого уровня, например тесты на уровне всей системы. Мы знаем, что такие тесты не являются адекватными, однако они прибавляют нам уверенности. Благодаря такой обратной связи мы можем сделать части кода, нуждающиеся в изменении, более удобными для внесения изменений.
Через некоторое время части системы, которые постоянно меняются, станут выглядеть так, как будто их разработали с использованием TDD. Мы медленно углубимся в дремучий лес наших старых ошибок.
Для кого предназначена методика TDD?
Каждая практика программирования явно или не явно базируется на системе ценностей. TDD не исключение. Если вам нравится лепить вместе куски кода, которые более-менее работают, и вы счастливы думать, что вам не придется возвращаться к полученному в результате этого коду в дальнейшем, значит, TDD – не для вас. Методика TDD базируется на очаровательно-наивном предположении программиста о том, что чем красивее код, тем вероятнее успех. TDD помогает вам обращать внимание на правильные вопросы в подходящие для этого моменты времени. Благодаря этому вы можете делать дизайн чище и модифицировать его по мере того, как перед вами встают новые обстоятельства.
Я сказал, что предположение наивное, однако, скорее всего, я преувеличил. На самом деле наивно предполагать, что чистый код – это все, что необходимо для успеха. Мне кажется, что хорошее проектирование – это лишь 20 % успеха. Безусловно, если проектирование будет выполнено из рук вон плохо, вы можете быть на 100 % уверены, что проект провалится. Однако приемлемый дизайн сможет обеспечить успех проекта только в случае, если остальные 80 % будут там, где им полагается быть.
С этой точки зрения TDD – чрезвычайно мощный инструмент. Если сравнивать со средним уровнем индустрии разработки программного обеспечения, методика TDD позволяет писать код, содержащий значительно меньше дефектов, и формировать значительно более чистый дизайн. Те, кто стремится к изяществу, могут найти в TDD средство для достижения цели.
Методика TDD также подходит для тех, у кого формируется эмоциональная привязанность к коду. Когда я был молодым программистом, самым большим разочарованием для меня была ситуация, когда проект начинался с огромным воодушевлением, а затем, с течением времени, код становился все более отвратительным. Год спустя у меня, как правило, формировалось устойчивое желание уйти из проекта, чтобы никогда в жизни не иметь дела с этим гнусно пахнущим кодом. TDD позволяет с течением времени поддерживать уверенность в коде. По мере того как тестов становится все больше (а ваше мастерство тестирования улучшается), вы обретаете все большую уверенность в том, что система ведет себя именно так, как вам надо. По мере того как вы улучшаете дизайн, становится возможным все большее количество изменений. Моя цель заключается в том, чтобы через год работы мне было бы интереснее и приятнее работать над проектом, чем в самом начале проекта, и TDD помогает мне достигнуть этой цели.
Зависит ли эффективность TDD от начальных условий?
Складывается впечатление, что разработка идет гладко только в случае, если тесты выполняются в определенном порядке. Тогда мы можем наблюдать классическую последовательность красный – зеленый – рефакторинг – красный – зеленый – рефакторинг. Вы можете попробовать взять те же самые тесты, но реализовать их в другом порядке, и у вас возникнет ощущение, что вы не сможете, как прежде, выполнять разработку маленькими шажками. Действительно ли одна последовательность тестов на порядок быстрее/проще в реализации, чем другая последовательность? Существуют ли какие-либо признаки тестов, которые могут подсказать, в какой последовательности их следует реализовать? Если методика TDD чувствительна к начальным условиям в малом масштабе, можно ли считать ее предсказуемой в более крупном масштабе? (Вот аналогия: отдельные потоки реки Миссисипи непредсказуемы, однако вы можете с уверенностью сказать, что через устье реки протекает приблизительно 2 000 000 кубических футов воды в секунду.)
Как методика TDD связана с шаблонами?
Все мои технические публикации – это поиск фундаментальных правил, которые позволяют обычным людям действовать так, как действуют эксперты. Это связано с тем, как я сам осваиваю то или иное ремесло, – я нахожу эксперта, которому можно подражать, и постепенно выясняю, что, собственно, он делает. Определенно, я не предполагаю, что сформулированные мною правила должны использоваться автоматически, однако именно так и происходит.
Моя старшая дочь (привет, Бетани! Я же говорил тебе, что ты попадешь в мою книгу, – не беспокойся, это не слишком обременительно) в течение семи лет пыталась научиться быстро перемножать числа. Как я, так и моя жена, когда были маленькими, научились этому за значительно более короткий срок. В чем дело? Оказывается каждый раз, когда перед Бетани вставала задача умножить, например, 6 на 9, она складывала число 6 девять раз (или число 9 шесть раз). Таким образом, можно сказать, что Бетани вообще не умела умножать числа так, как это делают другие люди, однако при этом она необычайно быстро складывала числа.
Я обратил внимание на один важный эффект, который, я надеюсь, смогут принять во внимание и другие. Если на основе постоянно повторяющихся действий формулируются правила, дальнейшее применение этих правил становится неосознанным и автоматическим. Естественно, ведь это проще, чем обдумывать все «за» и все «против» того или иного действия с самого начала. Благодаря этому повышается скорость работы, и, если в дальнейшем вы сталкиваетесь с исключением или проблемой, которая не вписывается ни в какие правила, у вас появляется дополнительное время и энергия для того, чтобы в полной мере применить свои творческие способности.
Именно это произошло со мной, когда я писал книгу Smalltalk Best Practice Patterns. В какой-то момент я решил просто следовать правилам, описываемым в ней. В начале это несколько замедлило скорость моей работы – мне требовалось дополнительное время, чтобы вспомнить то или иное правило или написать новое. Однако по прошествии недели я заметил, что с моих пальцев почти мгновенно слетает код, над разработкой которого ранее мне приходилось некоторое время размышлять. Благодаря этому у меня появилось дополнительное время для анализа и важных размышлений о дизайне.
Существует еще одна связь между TDD и шаблонами: TDD является методом реализации дизайна, основанного на шаблонах. Предположим, что в определенном месте разрабатываемой системы мы хотим реализовать шаблон «Стратегия» (Strategy). Мы пишем тест для первого варианта и реализуем его, создав метод. После этого мы намеренно пишем тест для второго варианта, ожидая, что на стадии рефакторинга мы придем к шаблону «Стратегия» (Strategy). Мы с Робертом Мартином занимались исследованием подобного стиля TDD. Проблема состоит в том, что дизайн продолжает вас удивлять. Идеи, которые на первый взгляд кажутся вам вполне уместными, позже оказываются неправильными. Поэтому я не рекомендую целиком и полностью доверять своим предчувствиям относительно шаблонов. Лучше думайте о том, что, по-вашему, должна делать система, позвольте дизайну оформиться так, как это необходимо.
Почему TDD работает?
Приготовьтесь покинуть галактику. Предположите на секунду, что TDD помогает командам разработчиков создавать хорошо спроектированные, удобные в сопровождении системы с чрезвычайно низким уровнем дефектов. (Я не утверждаю, что это происходит на каждом шагу, я просто хочу, чтобы вы немножко помечтали.) Как такое может происходить?
Отчасти этот эффект связан с уменьшением количества дефектов. Чем раньше вы найдете и устраните дефект, тем дешевле это вам обойдется. Иногда разница в затратах огромна (спросите у «Марс-лендера»). Снижение количества дефектов вызывает множество вторичных психологических и социальных эффектов. После того как я начал работать в стиле TDD, программирование стало для меня значительно менее нервным занятием. Когда я работаю в стиле TDD, мне не надо беспокоиться о множестве вещей. Вначале я могу заставить работать только один тест, потом – все остальные. Уровень стресса существенно снизился. Взаимоотношения с партнерами по команде стали более позитивными. Разработанный мною код перестал быть причиной сбоев, люди стали в большей степени рассчитывать на него. У заказчиков тоже повысилось настроение. Теперь выпуск очередной версии системы означает новую функциональность, а не набор новых дефектов, которые добавляются к уже существующим.
Уменьшение количества дефектов. Имею ли я право утверждать, что подобное возможно? Есть ли у меня научное доказательство?
Нет. На текущий момент не проводилось никаких исследований, подтверждающих преимущества TDD по сравнению с альтернативными подходами в смысле качества, эффективности или удовольствия. Однако эпизодические подтверждения преимуществ TDD многочисленны, а вторичные эффекты очевидны. При использовании TDD у программистов действительно снижается стресс, в командах действительно повышается доверие, а заказчики действительно начинают смотреть на каждую новую версию продукта с энтузиазмом. Лично мне не приходилось сталкиваться с обратными эффектами. Однако ваши наблюдения могут оказаться иными, вы должны попробовать для того, чтобы оценить TDD самостоятельно.
Еще одним преимуществом методики TDD, объясняющим ее положительные эффекты, является сокращение времени, которое проходит между принятием проектного решения и проверкой результата его реализации. В рамках TDD это достаточно короткий промежуток времени – несколько секунд или минут. Вы принимаете решение, реализуете его в коде, запускаете тесты и анализируете полученный результат. В начале у вас возникает мысль – возможно, API должен выглядеть так, или, возможно, метафора должна быть такой, – затем вы создаете самый первый пример – тест, который воплощает вашу мысль в реальность. Вместо того чтобы сначала проектировать, а затем в течение нескольких недель или месяцев ожидать, окажется ваше решение правильным или нет, вы получаете результат уже через несколько секунд или минут.
Причудливый ответ на вопрос, «Почему TDD работает?», основан на бредовом видении из области комплексных систем. Неподражаемый Флип пишет:
Следует использовать программистские практики, которые «притягивают» корректный код как предельную функцию, но не как абсолютную величину. Если вы пишете тесты для каждой присутствующей в системе функциональности, если вы добавляете в систему новые функции по одной, и только после того, как выполнятся все тесты, вы создадите то, что математики обозначают термином «точка притяжения (аттрактор)». Точка притяжения – это точка в пространстве состояний, к которой сходятся все потоки. Со временем код с большей вероятностью изменяется в лучшую сторону, а не в худшую; точка притяжения приближается к корректности, как предельная функция.
Это «корректность», которая устраивает всех программистов (за исключением, конечно же, тех, кто работает над медицинским или аэрокосмическим программным обеспечением). Я считаю, что важно быть знакомым с концепцией точки притяжения – ее не следует отвергать, ею не следует пренебрегать.
Что означает название?
Название методики: Test-Driven Development – разработка через тестирование. Буквально можно перевести как «разработка, ведомая тестами» или «разработка исходя из тестов».
Development (разработка) – старый поэтапный подход к разработке программного обеспечения обладает рядом недостатков, так как оценить результат проектного решения очень сложно, если решение и оценка результатов удалены друг от друга по времени. В названии TDD термин «разработка» означает сложную комбинацию анализа, логического проектирования, физического проектирования, реализации, тестирования, пересмотра, интеграции и выпуска.
Driven (исходя из, через) – в свое время я называл TDD термином test-first programming (программирование «вначале тесты»). Однако антонимом слова fist (вначале) является слово last (в конце). Огромное количество людей осуществляют тестирование уже после того, как они запрограммировали функциональный код. Этот подход считается вполне приемлемым. Существует любопытное правило именования, согласно которому противоположность придуманного вами имени должна быть, по крайней мере отчасти, неприятной или неудовлетворительной. (Термин «структурное программирование» звучит привлекательно, так как никто не хочет писать бесструктурный, то есть неорганизованный код.) Если в ходе разработки я исхожу не из тестов, то из чего? Из предположений? Домыслов? Спецификаций? (Обратите внимание, что слово «спецификация» немножко похоже на слово «спекуляция».)
Test (тест) – автоматическая процедура, позволяющая убедиться в работоспособности кода. Нажмите кнопку, и он будет выполнен. Ирония TDD состоит в том, что это вовсе не методика тестирования. Это методика анализа, методика проектирования, фактически методика структурирования всей деятельности, связанной с разработкой программного кода.
Как методика TDD связана с практиками экстремального программирования?
Некоторые из рецензетов данной книги, были обеспокоены тем, что книга целиком и полностью посвящена TDD, в результате читатели могут подумать, что остальными практиками XP (eXtreme Programming – экстремальное программирование) можно пренебречь. Например, если вы работаете в стиле TDD, должны ли вы при этом работать в паре? Далее я привожу перечень соображений относительно того, как остальные практики XP улучшают эффективность TDD и, наоборот, как TDD повышает эффективность использования других практик XP.
Программирование в паре. Тесты, разрабатываемые в рамках TDD, являются превосходным инструментом общения, когда вы программируете в паре. Зачастую, работая в паре, партнеры не могут договориться – какую именно проблему они решают, несмотря на то что работают с одним и тем же кодом. Это звучит бредово, однако подобное происходит постоянно, в особенности когда вы только осваиваете работу в паре. Именно этой проблемы удается избежать благодаря TDD. Существует и обратное влияние: когда вы работаете в паре, у вас есть помощник, который может взять на себя нагрузку в случае, если вы устали. Ритм TDD может исчерпать ваши силы, и тогда вы будете вынуждены программировать, несмотря на усталость. Однако если вы работаете в паре, ваш партнер готов взять у вас клавиатуру и тем самым дать вам возможность немного расслабиться.
Работа на свежую голову. XP рекомендует работать, когда вы полны сил, и останавливать работу, когда вы устали. Если вы не можете заставить следующий тест сработать или заставить работать те два теста одновременно, значит, настало время прерваться. Однажды мы с дядей Бобом Мартином (Bob Martin) работали над алгоритмом разбиения линии, и нам никак не удавалось заставить его работать. Несколько минут мы безуспешно бились над кодом, однако нам стало очевидно, что прогресса нет, поэтому мы просто остановили работу.
Частая интеграция. Тесты – это великолепный ресурс, который позволяет выполнять интеграцию значительно чаще. Вы добились успешного выполнения очередного теста, избавились от дублирования, значит, вы можете интегрировать код. Этот цикл может повторяться каждые 15–30 минут. Возможность частой интеграции позволяет более многочисленным командам разработчиков иметь дело с одной и той же базой исходного кода. Как сказал Билл Уэйк (Bill Wake): «Проблема n2 не является проблемой, если n всегда равно 1».
Простой дизайн. В рамках TDD вы пишете только тот код, который необходим для успешного выполнения тестов, вы удаляете из него любое дублирование, значит, вы автоматически получаете код, который идеально адаптирован к текущим требованиям и подготовлен к любым будущим пожеланиям. Общая доктрина требует, чтобы дизайна было достаточно для получения идеальной архитектуры для текущей системы. Эта доктрина также облегчает разработку тестов.
Рефакторинг. Устранение дублирования – это основная цель рефакторинга. Тесты дают вам уверенность в том, что поведение системы не изменится даже в случае, если в ходе рефакторинга вы вносите достаточно крупномасштабные изменения. Чем выше ваша уверенность, тем более агрессивно вы выполняете рефакторинг. Рефакторинг продлевает жизнь вашей системе. Благодаря рефакторингу вы упрощаете дальнейшую разработку тестов.
Частые выпуски версий. Если тесты TDD действительно улучшают MTBF вашей системы (в этом вы можете убедиться сами), значит, вы можете чаще внедрять разрабатываемый код в реальные производственные условия и при этом не наносить ущерба вашим заказчикам. Гарет Ривс (Gareth Reeves) приводит аналогию с куплей-продажей ценных бумаг на бирже в течение дня. Если вы занимаетесь краткосрочной спекуляцией ценными бумагами, в конце торгового дня вы должны продать все имеющиеся у вас ценные бумаги, так как вы не хотите принимать риск, связанный с сохранением некоторых ценных бумаг до следующего торгового дня, – этот риск вам не подконтролен. Разрабатывая систему, вы хотите, чтобы вносимые вами изменения как можно быстрее были опробованы в реальных производственных условиях, так как не хотите тратить время на разработку кода, в полезности которого не уверены.
Нерешенные проблемы TDD
Дарач Эннис (Darach Ennis) бросил вызов поклонникам TDD, размышляющим о возможностях расширения области применения TDD. Он сказал:
Множество различных организаций сталкивается с многочисленными проблемами TDD, и эти проблемы никак не затронуты в книге. Возможно, эти проблемы вообще никак не решить в рамках TDD. Вот некоторые из них:
• не существует способа автоматического тестирования GUI (например, Swing, CGI, JSP/Servlets/Struts);
• не существует способа автоматического тестирования распределенных объектов (например, RPC, Messaging, CORBA/EJB и JMS);
• TDD нельзя использовать для разработки схемы базы данных (например, JDBC);
• нет необходимости тестировать код, разработанный сторонними разработчиками, или код, генерируемый внешними инструментами автоматизации разработки;
• TDD нельзя использовать для разработки компилятора/интерпретатора языка программирования.
Я не уверен, что он прав, но я также не уверен, что он не прав. В любом случае это почва для размышлений о дальнейшем развитии TDD.