На момент написания данной книги браузер рефакторинга Refactoring Browser for Smalltalk по-прежнему является наилучшим инструментом в этой категории. В настоящее время многие среды разработки для Java поддерживают развитые средства рефакторинга. Кроме того, поддержка рефакторинга появилась и в других языках и средах разработки.
Флип предложил высказывание, которое может служить ответом на этот вопрос: «Пишите тесты до тех пор, пока страх не превратится в скуку». Высказывание подразумевает, что вы должны найти ответ сами. Однако вы читаете эту книгу для того, чтобы найти в ней ответы на вопросы, поэтому попробуйте воспользоваться следующим списком. Тестировать следует:
• условные операторы;
• циклы;
• операции;
• полиморфизм.
Однако только те из них, которые вы написали сами. Не тестируйте чужой код, если только у вас нет причин не доверять ему. В некоторых ситуациях недостатки (можно сказать жестче: «ошибки») во внешнем коде заставляют добавлять дополнительную логику в разрабатываемый вами код. Надо ли тестировать подобное поведение внешнего кода? Иногда я документирую непредсказуемое поведение (ошибку) внешнего кода при помощи теста, который перестанет выполняться, если в следующей версии внешнего кода ошибка будет исправлена.
Тесты – это канарейка, которую берут в угольную шахту, чтобы по ее поведению определить присутствие запаха плохого дизайна. Далее перечисляются некоторые атрибуты тестов, которые указывают на то, что дизайн тестируемого кода начинает плохо пахнуть:
• Длинный код инициализации. Если вы вынуждены написать сотни строк кода, создавая объекты для одного простого оператора assert(), значит, что-то не так, значит, ваши объекты слишком большие и их требуется разделить.
• Дублирование кода инициализации. Если вы не можете быстро найти общее место для общего кода инициализации, значит, у вас слишком много объектов, которые слишком тесно взаимодействуют друг с другом.
• Тесты выполняются слишком медленно. Если тесты TDD работают слишком медленно, значит, они не будут запускаться достаточно часто. Значит, программист будет в течение некоторого времени работать, вообще не запуская тестов. Значит, когда он их все-таки запустит, скорее всего, многие из них не сработают. На самом деле здесь кроется серьезная проблема: если тесты работают медленно, значит, тестирование частей и компонентов разрабатываемого приложения связано с проблемами. Сложности при тестировании частей и фрагментов приложения указывают на существование недостатков дизайна. Иными словами, улучшив дизайн, вы можете увеличить скорость работы тестов. (Продолжительность работы набора тестов не должна превышать десяти минут, по аналогии с ускорением свободного падения в 9,8 м/с2. Если для выполнения набора тестов требуется более 10 минут, этот набор обязательно надо сократить или тестируемое приложение должно быть оптимизировано так, чтобы для выполнения набора тестов требовалось не более 10 минут.)
• Хрупкие тесты. Если ваши тесты неожиданно начинают ломаться в самых непредсказуемых местах, это означает, что одна часть разрабатываемого приложения непредсказуемым образом влияет на работу другой части. В этом случае необходимо улучшить дизайн так, чтобы данный эффект исчез. Для этого можно либо устранить связь между частями приложения, либо объединить две части воедино.
Инфраструктура (framework) – набор обобщенного кода, который можно использовать в качестве базы при разработке разнообразных прикладных программ. На самом деле TDD является неплохим инструментом разработки инфраструктур. Парадокс: если вы перестаете думать о будущем вашего кода, вы делаете код значительно более адаптируемым для повторного использования в будущем.
Очень многие умные книги говорят об обратном: «кодируйте для сегодняшнего дня, но проектируйте для завтрашнего» (code for today, design for tomorrow). Похоже, что TDD переворачивает этот совет с ног на голову: «кодируйте для завтрашнего дня, проектируйте для сегодняшнего» (code for tomorrow, design for today). Вот что происходит на практике:
• В программу добавляется первая функциональность. Она реализуется просто и прямолинейно, поэтому реализация выполняется быстро и с наименьшим количеством дефектов.
• В программу добавляется вторая функциональность, которая является вариацией первой. Дублирование между двумя функциональностями объединяется и размещается в одном месте. Различия оказываются в разных местах (как правило, в разных методах или в разных классах).
• В программу добавляется третья функциональность, которая является вариацией первых двух. Уже имеющаяся общая логика, как правило, может использоваться в том виде, в котором она уже присутствует в программе, возможно, потребуется внести незначительные изменения. Отличающаяся логика должна располагаться в отдельном месте – в другом методе или в другом классе.
В процессе разработки мы постепенно приводим код в соответствие с принципом открытости/закрытости (Open/Closed Principle[28]), утверждающим, что объекты должны быть открыты для использования, но закрыты для модификации. Самое интересное, что при использовании TDD этот принцип выполняется именно для тех вариаций, с которыми действительно приходится иметь дело на практике. То есть TDD позволяет формировать инфраструктуры, удобные для представления таких вариаций, с необходимостью реализации которых программист сталкивается на практике. Однако эти инфраструктуры могут оказаться неэффективными в случае, если потребуется реализовать вариацию, которая редко встречается в реальности (или которая не была еще реализована ранее).
Что же произойдет, если необходимость реализации непредвиденной вариации возникнет спустя три года после разработки инфраструктуры? Дизайн быстро эволюционирует так, чтобы сделать вариацию возможной. Принцип открытости/закрытости на короткое время будет нарушен, однако это нарушение обойдется относительно недорого, так как имеющиеся тесты дадут вам уверенность в том, что, изменив код, вы ничего не поломаете.
В пределе, когда вариации возникают достаточно быстро, стиль TDD невозможно отличить от заблаговременного проектирования. Однажды я всего за несколько часов с нуля разработал инфраструктуру составления отчетов. Те, кто следил за этим, были абсолютно уверены, что это трюк. Они думали, что я сел за разработку, уже имея в голове готовую инфраструктуру. Однако это не так. Просто я долгое время практиковал TDD, благодаря этому я исправляю допущенные мною многочисленные ошибки быстрее, чем вы успеваете заметить, что я их допустил.
Насколько емкой должна быть обратная связь? Рассмотрим простую задачу: дано три целых числа, обозначающих длины сторон треугольника. Метод должен возвращать:
• 1 – в случае, если треугольник равносторонний;
• 2 – в случае, если треугольник равнобедренный;
• 3 – в случае, если треугольник не равносторонний и не равнобедренный.
Если длины сторон заданы некорректно (невозможно построить треугольник со сторонами заданной длины), метод должен генерировать исключение.
Вперед! Попробуйте решить задачу (мое решение, написанное на языке Smalltalk, приведено в конце данного подраздела).
Это отчасти напоминает игру «Угадай мелодию» («Я могу закодировать задачу за четыре теста!» – «А я – за три!» – «О’кей попробуйте».) Для решения задачи я написал шесть тестов, а Боб Биндер в своей книге Testing Object-Oriented Systems[29] («Тестирование объектно-ориентированных систем») для этой же самой задачи написал 65 тестов. Сколько на самом деле нужно тестов? Вы должны решить это сами, исходя из собственного опыта и рассуждений.
Когда я думаю о необходимом количестве тестов, я пытаюсь оценить приемлемое среднее время между сбоями (MTBF, Mean Time Between Failures). Например, в языке Smalltalk целые числа ведут себя как целые числа, а не как 32-битные значения. Иными словами, максимально возможное значение целого числа ограничивается не тридцатью двумя битами, а объемом памяти. Это означает, что вы можете обойтись без тестирования MAXINT. Безусловно, определенный предел существует, ведь теоретически можно создать целое число, для хранения которого не хватит имеющейся памяти. Но должен ли я тратить время на написание и реализацию теста, пытающегося заполнить память невероятно огромным целым числом? Как это повлияет на MTBF моей программы? Если я в обозримом будущем не собираюсь иметь дело с треугольниками, размер сторон которых измеряется такими числами, значит, моя программа не станет существенно менее надежной, если я не реализую такой тест.
Имеет ли смысл писать тот или иной тест? Это зависит от того, насколько аккуратно вы оцените MTBF. Если обстоятельства требуют, чтобы вы увеличили MTBF от 10 лет до 100 лет, значит, имеет смысл уделить время для разработки самых маловероятных и чрезвычайно редко возникающих ситуаций (если, конечно, вы не можете каким-либо иным образом доказать, что подобные ситуации никогда не могут возникнуть).
Взгляд на тестирование в рамках TDD прагматичен. В TDD тесты являются средством достижения цели. Целью является код, в корректности которого мы в достаточной степени уверены. Если знание особенностей реализации без какого-либо теста дает нам уверенность в том, что код работает правильно, мы не будем писать тест. Тестирование черного ящика (когда мы намеренно игнорируем реализацию) обладает рядом преимуществ. Если мы игнорируем код, мы наблюдаем другую систему ценностей: тесты сами по себе представляют для нас ценность. В некоторых ситуациях это вполне оправданный подход, однако он отличается от TDD.