Alexander Beletsky's Development Blog: 2008-03

Разработка ведомая тестированием. Часть 2. Зачем?

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

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

  1. Превращение гипотезы в модель.
  2. Устойчивость к изменениям и регрессии.
  3. Сокращение времени на обнаружение ошибок и отладку.
  4. Улучшение качества кода.
  5. Ускорение процесса разработки.

Превращение гипотезы в модель.

Используя принцип "Сначала тест", вы строите предположения относительно проектируемой вами сущности. Эти предположения, выражаются в виде набора тестов (предположения могут выдвигаться и по одному, соотвественно, создавая один тест и добавляя их в процессе расширения). Согласно принципу бритвы Окама создается минимальное количество предположений, способное объяснить ту или иную гипотезу. Далее итерируя в цикле TDD, вы приходите к реализации, тем самым пройдя путь от гипотезы к модели.

Устойчивость к изменениям и регрессии.

Так как весь программный код создается одновременно с тестами (точнее, тесты создаются сначала), мы получаем довольно прочный фундамент на котором стоит создаваемая система. Обширный и качественный набор юнит тестов и интеграционных тестов является надежным щитом, как и для случайных изменений (внесенных ошибочно), так и явных изменений, которые могут косвенно влиять на работоспособность других компонентов системы. Это может не так остро ощущаться на ранних этапах конструирования, так как существует еще не большое количество связанных сущностей, логика реализации, как правило, довольно понятна, что уменьшает вероятность внесения случайных ошибок. Внесение изменений на поздних этапах конструирования, или после выпуска продукта, на этапе поддержки, представляет собой более серьезную проблему. Если система уровня корпоративного приложения, мы имеем: сложную бизнес логику, большое количество зависящих друг от друга компонент, набор различных конфигураций для приложения. Любое, даже самое минимальное изменение представляет собой потенциальную опасность. При отсутствии автоматических тестов, разработчик внося изменение проверяет его на каком-то ограниченном сценарии использования, при этом косвенное влияние изменения на другие компоненты остаеться для него не замеченным.. Такого рода ошибки, огромная головная боль. Тестировщик находит (если повезет, если нет, находит заказчик), такую ошибку, поведение которой и близко не соотвествует тому изменению, которое было внесено.. создает тест репорт, который попадает в руки менеджеру, который в свою очередь может отдать этот дефект другому разработчику.. ну а тот проведет 2 дня отлаживая и находит изменение, которое сделано совсем не им, и даже не в зоне его ответственности. При этом проект уже может находиться в стрессой ситуации. В команде паника, и желание покинуть скорее этот проект. При наличии качественного набора тестов, вероятность такого поворота событий значительно снижается. Я не могу сказать о том, что любой неверный (или неожиданный) шаг может быть выявлен тестами. Если тесты проходят, это не означает, что ошибок нет, это означает, что тесты их не выявили. Однако, опыт показывает, что проект разработанный про правилам TDD, более устойчив. Мне приходилось работать над крупной системой, с очень хорошим покрытием кода тестами. И не один раз случалось так, что работая над какой либо проблемой и внося изменения, я получал фейлы тестов, которые зависят от работоспособности того класса, который я менял.. При этом я понятия не имел, о такой связи. Изменения, которые были внесены, были неожиданны, для других компонент - как правило, я либо пересматривал те изменения, которые внес, либо изменял зависящую компоненту, чтобы она сохранила свою работоспособность. Интеграционные тесты, создают своего рода tracebility, для всего кода. Тоже самое касается и регрессии. TDD накладывает правила и на багфиксинг - согласно этому правилу любой дефект исправляется следующим образом: создается тест, доказывающий существование этого дефекта, дефект исправляется, код и тест заливаются в систему контроля версий. Следуя такой практике, мы гарантируем отсутствие этого дефекта в будущем и как следствие регрессионному проявлению данного дефекта.

Сокращение времени на обнаружение ошибок и отладку.

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

Улучшение качества кода.

Для разработчиков не пишущих тесты это явно удивительное утверждение. Как тестирование может влиять на качество кода? А на самом деле, речь идет о следующем: разрабатывая тест сначала, вы волей неволей, становитесь пользователем того кода, который разрабатываете, соотвественно ваше мышление становится мышлением пользователя (пользователя, вмысле другого программиста, использующего код). Это ведет к тому, что разрабатываемые сущности более строго соответствуют принципу одиночной ответственности, интерфейсы более ограненные, не содержат ничего лишнего, а лишь то, что в точности необходимо клиенту. К этому стоит добавить то, что написание теста сначала провоцирует использовать большее количество фабрик и интерфейсов. Методы классов, как правило, не очень большие, позволяемые их качественно тестировать. Ну и самое главное - TDD, это постоянный рефакторинг кода.. постоянное увеличение качества.

Ускорение процесса разработки.

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

Разработка ведомая тестированием. Часть 1. Описание

Разработка ведомая тестированием , более известна под английской аббревиатурой TDD (Test driven development) это одна из техник разработки программных продуктов, суть которой достаточно прозрачна, и может быть представлена в виде такой итерации:

Тест -> Реализация -> Рефакторинг

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

Предполагается следующий подход:

 

Проектирование теста. На этом этапе вы задумываетесь о том, какую функциональность вы хотите получить от данной сущности и (самое важное) - как вы можете проверить эту функциональность. По сути дела, вы выносите гипотезу, которую пытаетесь опровергнуть/подтвердить.

Написание теста.Гипотеза рожденная на первом этапе выражается в виде програмного кода. Создаются необходимые файлы, содержащие код тестов. Тесты должны быть доступны в любой момент для запуска.

Красный результат (попытка №1). При первой проверке гипотезы, вы получаете красный результат (red result), свидетельствующий о том, что вынесенная ранее гипотеза о функционировании не верна. Если вы используете компилирущийся язык, со статической проверкой типов, то компиляция не проходит.. если какой либо из видов динамических языков, то имеете исключение времени-выполнения. Это естественно, ибо на данный момент времени даже не существует той сущности (сущностей), которая может быть ответственна за данную гипотезу.

Реализация сущности (создание макета). Прошу обратить внимание на тот факт, что шаг 4 не есть шагом "проектирования".. ведь, на самом деле, он не нужен, потому что все проектирование было сделано на этапе 1, в том момент, когда проектировался тест. С проектированием теста, мы уже получаем примерный дизайн сущности, очерчиваем ее интерфейсную часть.. ставим условия на ее поведение. В тоже время, на данном этапе всегда имеет смысл создание лишь макета.. или заглушки (реализации интерфейса, с "пустыми" методами, либо методами выбрасывающими исключения).

Красный результат (попытка №2). Гипотеза проверяется еще раз (тесты запускаются еще раз). Этот шаг несет следующий смысл - вы запускаете тесты (ошибок компиляции, или исключений времени выполнения).. и еще раз убеждаетесь в том, что они выдают красный результат (не проходят). Если тест или тесты проходят, то это означает то, что во время проектирования либо реализации была допущена ошибка - либо тест не делает правильных проверок (так называемых Asserts), либо эти проверки не адекватны ожидаемому поведению. Как правило, к этому моменту времени, разработчик имеет более четкую картину того, чего он на самом деле хочет получить, по сравнению с шагом №1. Это справедливо во многих случаях, даже если с момента начала этапа №1 до этапа №5 прошли считанные минуты. Поэтому, вносятся изменения в уже существующие тесты, добавляется новые, корректируются созданный ранее макет.

Реализация сущности (заполнение макета функциями). Давайте обернемся назад.. и посмотрим на то, что мы имеем на данный момент. А мы имеем следующее - тест (или тесты), макет, и что самое главное понимание того, что ожидается к реализации, и в тоже время идея как можно это сделать! Именно так.. я сделал вывод для себя, что идя по пути 2 - 5 я не просто занимаюсь полу-механической работой: создание макетов, запуск тестов, коррекция.. а в это время, явно или не явно, я продумываю детали реализации того или иного метода. Поэтому картина того, что мы имеем добавляется красивой, гравированной рамкой - "Идеей о реализации"! Мы имеем довольно прочный фундамент.. понятный, строгий, устойчивый. Дальнейший процесс есть обычная девелоперская рутина (c которой начинает разработчик, без использования TDD теряясь в мыслях, и не совсем понимая куда же он движется). Добавлю несколько ньюансов к процессу разработки:

  • следуйте принципу KISS, делайте все как можно проще.. это относиться ко всему, даже если код, которые получается при реализации не очень "хорош", смело идите на этот шаг, чуть позже я опишу почему. Не забывайте о цели, цель дойти в точку назначения (получить прохождение тестов) любым путем!
  • разрабатываете только те функции, которые могут быть проверены тестами.. если функция не проверяется тестом, добавте тест сначала; следуйте 3 важным правилам TDD.
  • даже с небольшими изменениями перезапускайте тесты, и смотрите на результат;

Зеленый результат (попытка №1). Это первый успешный майлстоун TDD (хотя, я не говорю о том, что красный результат это свидетельство о неуспехе). Мы превратили гипотезу, сначала в идею, а потом в работающую модель! При этом мы всегда шли отталкиваясь от начальной гипотезы, действуя в ее рамках. Мне особенно хотелось подчеркнуть этот факт: именно превращение гипотезы в модель, есть самое главное достоинство TDD остальные факторы (code coverage, change's appy, self-documented code) являются крайне важными, но не главенствующим. Им я посвящу одну из следующих статей в блоге.

Рефакторинг. Помните, что я говорил на этапе 6 - действуйте как можно проще, даже если это идет в ущерб code quality. Теперь, когда вы стоите на прочном фундаменте тестов, проявляйте все свои самые сильные стороны разработчика - будте гуру паттернов проектирования, знатоком "Совершенного кода", адептом "Защищенного программирования" и т.д. Делайте ваш код идеальным. Если вы допустите ошибку в процессе рефакторинга, тесты немедленно подскажут вам об этом, при первом же их запуске (а не через 2 дня, получая тест репорт с фейлом.. или письмо от кастомера..). Тесты ваш помошник, который постоянно рядом но может говорить всего две фразы: "Все ОК", "Нет.. тут кажется проблема", но этот помошник не гений, думающий за вас.. он действует только в тех рамках, которые вы сами ему установили.

Зеленый результат (попытка №2). Код вылизан, он настолько красив и грамотен, что вечером за пивом вы рассказываете о нем своим друзьям! Вам приятно от осознания факта, что вы можете делать делать что-то хорошо.

Коммит в систему управления версиями. После коммита в систему управления версиями код и тесты компилируются и запускаются каждый раз, когда идет билд продукта.. и с каждым зеленым результатом говоря - "ты хорошо сделал свою работу!". Вы морально удовлетворены, потому что перед коммитом у вас не дрожат руки с мыслью: "а не забыл ли я ничего проверить?", "хорошо ли я выполнил девелопмент тестирование?" (а я знаю точно - разработчик часто что-то забывает, а к девелопмент тестированию почти всегда относится халатно), вы вовремя уходите с работы и спите крепким сном :).

 

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

И в качестве резюме:

  • TDD это способ превращения гипотезы в модель.
  • TDD это способ повышения self-confidence в собственностей работе.
  • TDD это весело!

Первое сообщение

Меня зовут Александр Белецкий и я начинаю свой блог на blogspot.com. Моя профессиональная деятельность связана с разработкой програмного обеспечения. Как и любой другой разработчик, день изо дня, я сталкиваюсь с новыми интересными вещами.. и небольшими "открытиями", которые происходят в процессе работы. Цель данного блога это knowledge keeping&sharing. Я постараюсь освещать те вещи, которые могли бы быть интересны другим разработчикам.. а также сохранить знания и опыт, полученные в процессе работы над той или иной задачей.