От переводчика: так сложилось, что в русскоязычном интернете мало информации о 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 с помощью любого достаточно глубокого автоматического теста).
Но самым важным здесь является создание наброска будущей архитектуры. Просто делается это не на доске для рисования (что имеет свои преимущества), а прямо в коде, что позволяет незамедлительно понять, насколько просто использовать и тестировать этот код. Посмотрим, какие вопросы возникают при создании самого первого, несрабатывающего теста:
При создании кода все перечисленные вопросы когда-нибудь должны быть заданы, и сила TDD в предоставлении удобного инструмента для этого. Вместо процесса поочередного решения вопросов с предотвращением потенциальных конфликтов по мере их возникновения, TDD позволяет задать абстрактный вопрос «Как мне написать следующий тест?» и в процессе его решения ответить на ряд конкретных вопросов. Вот почему TDD не лучше программиста – все равно придется искать ответы на те же самые ключевые вопросы, что требует соответствующих знаний и опыта. TDD просто облегчает поиск.
Что именно делает упомянутый выше абстрактный вопрос? Как и любая хорошая абстракция, он дает возможность рассматривать множество мелких составляющих как одно целое, при создании которого можно разом применить все имеющиеся технические и другие навыки. Кроме того, поиск ответа на этот единственный вопрос облегчает еще одно свойство TDD: предоставление быстрой и точно обратной связи с кодом.
Процесс создания теста дает всестороннюю оценку разрабатываемому модулю. Подготовка к использованию чересчур громоздка? Возможно, у нас слишком много вспомогательных классов (или они нарушают Закон Деметры) и можно попытаться скрыть их за дополнительной абстракцией. Получается многовато сценариев использования или фикстур? Вероятно, тестируемый код имеет слишком много обязанностей. Трудно изолировать тестируемое поведение или непонятно, как его проверить? Наверное, у нас неправильное API или сама абстракция, и ее надо выделить как-то иначе. С TDD эти проблемы становятся очевидными мгновенно. Их решение требует навыков проектирования, таких же, каких требуют другие методики разработки. Но создание теста в самом начале дает отличную возможность отреагировать на ошибки и проверить проект модуля до его реализации. Самое дешевое время для исправления кода – до его написания.
Затем мы пишем самый простой код, удовлетворяющий тесту. Как ни странно, это наименее интересная часть 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, что попытались сделать слишком большой шаг в разработке модуля. Реализация тривиальна и имеет очевидные недоработки? Отлично, теперь мы знаем, каким будет следующий тест.
И вот мы на шаге рефакторинга. Только что созданный код удовлетворяет тесту, но мы были сфокусированы на очень маленьком участке приложения и сейчас самое время охватить взглядом всю картину. Если было реализовано решение «в лоб», то можно избавиться от дублирования или выделить отдельный метод, чтобы код лучше описывал то, что он делает. Еще более важным является выявление высокоуровневого дублирования – повторений не просто участков кода, а схожего поведения, которое может быть выделено в абстракцию или выведено на структурный уровень.
Более конкретно о том, что со всем этим делать – в продолжении.