Что пишут в блогах

Подписаться

Онлайн-тренинги

Конференции

Что пишут в блогах (EN)

Разделы портала

Про инструменты

.
Ошибки начинающих TDD-практиков
29.09.2008 11:09

Автор: Сергей Юдин

Опубликовано с согласия www.phpinside.ru

С момента проведения конференции в Киеве в мае 2005 года прошло достаточно много времени. Тогда мы (я и Павел Щеваев) постарались приложить максимум усилий для популяризации идеи TDD среди PHP-разработчиков. За время после конференции мы намного продвинулись вперед в плане TDD, и взгляд на некоторые вещи у нас изменился. Я признаю, что мой доклад на тему «Целесообразность модульных тестов» получился немного однобоким. Вообще на конференции было очень много сказано о плюсах тестирования, как здорово тесты помогают в разработке и рефакторинге, в профессиональном развитии программистов, но слишком мало – о минусах. О минусах и очень больших минусах тестирования, которые могут проявиться, если трактовать и использовать TDD неправильно.

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

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

Правильно понимаем TDD

Начнем с того, что большинство разработчиков неправильно понимает идею TDD. Самое типичное недоразумение, связанное с TDD: «Мы используем в своей работе SimpleTest или PhpUnit, значит, мы работает в стиле TDD.» Этот совсем не обязательно. Люди, которые пишут (или пытаются написать) модульные тесты для своих классов — не обязательно занимаются TDD.

Как-то в одной из TDD-рассылок я наткнулся на некую классификацию способов применения модульных тестов разработчиками:

  1. Традиционный — когда разработка ведется полностью через тесты, один тест за раз, при этом активно применяется рефакторинг. Первоначальная реализация рабочего кода обычно является нарочно упрощенной, конечный дизайн кода получается последовательно и только исходя из появления новых тестов
  2. Активный — отличается от традиционного тем, что разработчик сначала обдумывает дизайн рабочего кода, а затем начинает целенаправленно идти к этому дизайну через тесты.
  3. Приемочный — вместо того, чтобы писать небольшие тесты, разработчик пишет сразу конечный тест, который реализует конечную функциональность. Далее он продумывает дизайн реализации, и всю несуществующую функциональность забивает мок-объектами, стараясь запустить тесты как можно раньше. После этого постепенно убирает мок-объекты, заменяя их реальными классами.

Только первый вариант можно считать полноценным TDD. Второй вариант — это так называемая методика test-first development, то есть TDD без первой D — driven. Третий вариант никакого отношения к самому TDD не имеет, а должен применяться параллельно в виде приемочных тестов.

  TDD — это процесс итеративного, непрерывного, параллельного написания тестов и рабочего кода, с обязательными фазами рефакторинга.

Очень многие разработчики начинают сразу с набора модульных тестов для нового класса, а лишь только затем пишут сам класс. В итоге они тратят полдня на то, чтобы эти тесты сработали. Им приходится отлаживать не только рабочий код, но и тесты, при этом любое изменение структуры заставляет их переписывать большие участки тестового кода — это нудно, неинтересно и занимает много времени. Это вовсе не TDD и даже не test-first. Когда вы занимаетесь TDD, вы должны стремиться минимизировать размер контекста, в котором вы работаете. Это относится как к тесту, так и к рабочему коду. Мы обычно пишем тесты очень мелкими шагами и не пытаемся представить, как будет выглядеть окончательный код. Это позволяет очень рано увидеть зеленую линию и обойтись без debug сессии при начальном запуске тестов. Даже если и так понятно, как будет выглядеть все остальные тесты и сам рабочий код класса, мы предпочитаем написать короткий тест, затем небольшую часть рабочего кода. Затем можно двигаться быстрее и увереннее. Конечно, каждый самостоятельно может определять, что для него «небольшие шаги», но общая рекомендация такая — старайтесь сокращать промежутки между запусками тестов. При активной разработке мы обычно запускаем тесты раз в 2-3 минуты и даже чаще. Это позволяет проверять написанный код сразу же, не теряя контекста, то есть, помня все детали только что измененного кода.

Рассмотрим «активный» и «приемочный» способы использования тестов (см. выше). Мы начинали свою практику TDD именно с «активного» способа применения модульных тестов, рисуя UML-диаграммы, а затем старались при помощи тестов реализовать свои задумки в коде. Но постепенно мы отказались от повседневного применения этой практики. Дело в том, что если разрабатывать, ставя во главу test-ability, то есть возможность протестировать код, то полученная реализация всегда будет отличаться от задуманной, а времени, затраченного на детальную проработку дизайна, становится жаль. Теперь мы стараемся не тратить больше 10-15 минут на дизайн-сессии, достаточно пары небольших набросков на доске, которые дадут ориентировочную картину, и можно приступать к кодированию. Кстати, я вовсе не хочу сказать, что TDD заменяет фазу анализа проекта. Архитектурные решения, оказывающие влияние на весь проект в целом, все равно нужно принимать. В конце концов, TDD никак не может заменить аналитические способности разработчика, его умение предугадывать развитие проекта. Но TDD реально позволяют выбраться из ситуации архитектурного тупика (design deadlock), когда вообще непонятно, как должна выглядеть реализация.

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

Обязательным условием успешного внедрения TDD является фаза рефакторинга. После получения рабочего кода и зеленой полосы необходимо обязательно критически посмотреть на код и сделать хотя бы несколько шагов для улучшения читабельности и понятности кода. Идеально, если после рефакторинга код вообще бы не требовал inline комментариев. Важно также как можно чаще при этом запускать тесты, чтобы не сталкиваться с красной полосой. В книге Мартина Фаулера «Рефакторинг: улучшение дизайна существующего кода» техника рефакторинга описана очень подробно.

Правильно внедряем TDD

Разработку через тестирование никак нельзя освоить за пару недель. По нашему опыту и опыту некоторых других разработчиков на первоначальное изучение методик тестирования уходит где-то 3-4 месяца. Еще полгода-год нужно на перестройку сознания разработчика. Итого — около года, иногда дольше. Почитайте книги по TDD – это 60% код и еще 35% - комментарии к ним и 5% теории. TDD — это набор лучших практик (best practices), самым лучшим образом зарекомендовавших себя в разработке программного обеспечения. Многие из них нужно попробовать на себе, чтобы понять их важность. TDD должен стать каждодневной реалией вашего рабочего процесса. При этом вам обязательно придется столкнуться с трудностями. Невозможно в кратчайшие сроки быстро научиться писать хорошие тесты, создавать оптимальное количество тестов, двигаться ровными шагами (тест-код-тест-код-рефакторинг), правильно проводить изоляцию тестов, давать правильные имена методам и переменным и т.д. Именно поэтому желательно, чтобы в команде был человек (в XP он называется тренер - coach), который имеет опыт тестирования, особенно при внедрении модульного тестирования в готовый проект. Именно наличие такого человека поможет избавиться от многих проблем, связанных с введением TDD, например, с появлением запахов тестового кода. Тренер может в разы сократить сроки внедрения TDD в какую-либо команду. Мы много раз ловили себя на том, что неправильно используем тесты, неправильно проводим большие рефакторинги, теряем ритм разработки и т.д. Только по прошествии 2-х лет очень интенсивной практики TDD мы можем с сказать, что TDD гармонично слилась с нашим рабочим процессом, и мы можем пожинать плоды ее использования.

Многие разработчики (и мы в том числе) заметили, что внедрять TDD лучше всего или на новом проекте (можно учебном), или же с отдельных классов. Мы вообще не рекомендуем начинать внедрение TDD на реальных коммерческих проектах, особенно на больших и со сложным наследием. Без унаследованного кода (legacy code) внедрять TDD намного проще, так как в этом случае есть возможность полностью контролировать ситуацию и принимать какие угодно решения. Наш (и не только наш) опыт подсказывает, что в первые месяцы внедрения TDD появляется сильнейшее желание переделывать одну и туже работу по несколько раз, так как происходит смена мировоззрения разработчика - он начинает осознавать настоящие преимущества того или иного дизайна. Поэтому свобода действий является очень важным критерием успешности на начальном этапе.

Идеально, если на начальном этапе внедрения TDD вы не будете ограничены сроками. Первое время при внедрении TDD происходит значительное снижение скорости работы — где-то в 2-3 раза, поэтому многие разработчики срываются и пишут код без тестов. После того, как вы освоите большинство продвинутых методик тестирования, скорость снова возрастет и даже будет выше, чем раньше.

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

Начинать внедрение модульного тестирования лучше небольшими шагами и по направлению снизу вверх. То есть сначала тестировать библиотечные классы нижних уровней. При этом разработчик должен сначала привыкнуть к тестам, научиться читать их, использовать их в качестве документации к коду, рассматривать классы с точки зрения возможности их протестировать. В этом случае разработчик будет иметь дело с небольшими модулями (unit), а это действительно важно на начальном этапе. Лишь много позже можно переходить к более сложным случаям, например, к тестам взаимодействия, к расширенному применению мок-объектов, к регрессионным тестам.

Если же у вас появились серьезные проблемы с тестами, посмотрите на список наиболее часто появляющихся запахов тестового кода — может быть вы наткнулись на один или сразу нескольких из них. Это позволит понять причины своих неудач и не бросить начатое. Список запахов тестового кода доступен в разделе TDD на сайте phpclub.ru

Расширяем свои познания об ООП

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

Как-то на форуме сайта sitepoint.com я наткнулся на очень интересное высказывание Маркуса Бейкера (создателя пакета для тестирования SimpleTest) насчет объектно-ориентированного программирования. Суть этого высказывания сводится к тому, что большинство разработчиков начинает использование ООП, не разобравшись до конца (или вообще не разобравшись) в его сути. Начинающий (в плане опыта применения ООП) программист, познакомившийся с концепциями (или даже просто с синтаксисом) ООП, однажды создает несколько классов, каждый из которых потянет на целый пакет и после этого сразу начинает проектировать большую систему, возомнив себя крутым архитектором. Конечно, у него ничего не получается. После этого такие разработчики читают множество книг по паттернам и застревают еще больше, так как не понимают, зачем эти паттерны нужны, и где правильно их применять. Классы становятся обузой и многие их них переходят обратно к процедурному программированию, так как не находят в классах ничего хорошего. Опытные разработчики, по словам Маркуса, используют ООП для того, чтобы не проектировать вовсе! Такие понятия, как повторное использование кода давно устарели. Классы нужно любить за их гибкость, а не за возможность повторного использования. Чтобы понять это нужно очень много времени и упорства или же наличие опытного наставника.

Мы начинали практику TDD осенью 2003 года с типичной ситуации, описанной Маркусом Бейкером. У нас была своя доморощенная CMS, где каждый класс был богоподобным, везде встречались такие модные словечки, как Factory, Singleton, Fasade и т.д. Но это была ужасная система. И в этой ситуации мы начали внедрение TDD. Угадайте к чему мы пришли через полгода? Практически к тому же самому! Только теперь к ужасному коду (правда тогда мы так не считали!) добавились еще и неповоротливые тесты. По наивности мы думали, что написание тестов — это и есть TDD. В итоге мы получили закостеневшую и неповоротливую систему. После этого мы поняли, что без коренной перестройки наших знаний об ООП успеха нам ждать не следует, и мы реально учились многому с нуля, открывая многие вещи заново, например, паттерны проектирования.

Вот список книг, которые любой TDD-практик просто обязан прочитать (must read) и иметь в любой момент на своем столе:

Все эти книги есть на русском языке. Я уверен, что по мере совершенствования ваших навыком модульного тестирования вам захочется перечитать эти книги не раз и не два. Также очень полезной оказалась книга V. Manson «Junit In Action», к сожалению ее пока нет на русском языке. Несмотря на то, что она написана для Java, многие идеи, описанные в этой книге, применимы к любому языку.

Любой продвинутый TDD-практик (да в принципе любой программист) должен отлично понимать, что такое зависимости между отдельными классами и подсистемами, как эти зависимости снижать, какие при этом могут использоваться методики, что такое клиент-ориентированный API и т.д. Вы должны стремиться к пониманию сути большинства базовых патеров, например, Декоратор или Стратегия, в каких случаях их следует применять. Мне лично очень сильно помогла в плане ООП развития книга Р.Мартина «Быстрая разработка программ» (Agile software development). Кстати, когда мы только начинали практику TDD, нам казалось, что мы очень отлично подкованы в плане ООП, однако оказалось, что это далеко не так. Кстати, это заметили не только одни мы. Люди, которые имеют опыт программирования 5 и более лет, по прошествии лишь 3-4 месяцев практики TDD признают, что тесты заставили их посмотреть на многие вещи другими глазами. Написание тестов очень наглядно показывают целесообразность многих ООП методик, которые многие знают и так, но эти знания лежат у них обычно мертвым грузом.

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

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

TDD — это не самоцель

Тесты — это не вещь в себе. Но часто разработчики впадают в крайность — это ситуация, когда тестам начинают уделять повышенное внимание. Разработчики пишут слишком много тестов, проверяют все и вся. И это однажды перерастает в разработку ориентированную на тесты (testing oriented development anti-pattern). Джемс Гринвуд так описывает этот анти-паттерн: «Когда неопытные или введенные в заблуждение программисты продолжают писать тесты, хотя в этом нет никакого смысла с практической или финансовой точки зрения. При этом они думают, что это и есть TDD и, добавляя в систему все новые и новые тесты, они увеличивают ее стоимость». Думается, что комментировать это определение нет смысла.

Попробую описать другой случай, когда тесты становятся самоцелью и мешают развитию проекта. Иногда это связывают с запахом «Чрезвычайное упования на мок-объекты», но иногда причина кроется в другом. Мы сталкивались с данной проблемой достаточно давно, но последствия дают о себе знать до сих пор. Суть ее такова: у нас был код, который очень трудно поддавался тестированию, так как он не был создан с учетом test-ability. Но, не понимая сути TDD, мы вместо того, чтобы искать причины в дизайне системы, делали все возможное, чтобы просто написать тесты. В результате мы получили полный хаос с частичными или полными мок-объектами, методами вида doParentSomething(), непонятными и огромными фикстурами. То есть мы плодили ненужный код, который тоже нужно было поддерживать. При этом тесты являлись дополнительным фактором загнивания проекта, так как они были очень хрупкими, хотя по идее они должны были напротив повышать его гибкость и качество. Лишь намного позже мы поняли, что тесты наглядно нам показывали, что у нас есть большие проблемы c дизайном рабочего кода. Хороший рабочий код не должен иметь сложных тестов! Это показатель того, что архитектура имеет серьезные недостатки. После правильного применения техник снижения зависимостей между классами, тесты значительно упростились, и их поддержка теперь не составляет труда.

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

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

Выводы

Разработка через тестирование для меня была чем-то вроде серебряной пули, решением всех проблем. Я готов был с горящими глазами доказывать, что тесты — панацея от всех бед, что нужно всем скорее начинать практику написания тестов. Теперь пришло небольшое прозрение. Сейчас я понимаю, что это всего лишь инструмент, а то, как этот инструмент используется, зависит от человека, который это инструмент держит в руках. В чьих-то руках он может приносить много пользы, а в других — много вреда. Если вам нравится все то, что сулит TDD, то будьте готовы к жертвам: это и потеря скорости разработки в первое время, и жесткая дисциплина и самоконтроль, интенсивное обучение и т.д. Все эти жертвы потом с лихвой окупятся. Многие же не выдерживают и возвращаются к прежнему стилю работы. Но, по нашему мнению, а также по мнению некоторых знакомых нам TDD-практиков, как только разработчик почувствовал зависимость от «зеленой полосы», узнал, что такое полный контроль за кодом, то есть стал инфицированным тестами (test-infected), он уже никогда не откажется от тестирования. Пока никто из наших знакомых не жалеет времени, потраченного на изучение и эксперименты с TDD. Я очень надеюсь, что эта статья поможет кому-то ускорить внедрение TDD и избежать тех ошибок, которые мы допускали.