Автор книги: Джонатан Расмуссон
Жанр: Зарубежная деловая литература, Бизнес-Книги
Возрастные ограничения: +12
сообщить о неприемлемом содержимом
Текущая страница: 12 (всего у книги 13 страниц)
Глава 14
Разработка на основе тестирования
Вы забуксовали. Оказались в тупике. Вы целый день взираете на один и тот же фрагмент кода и просто не знаете, как его разобрать и даже с чего начать.
Если бы вы писали код, как Эрик!
Его код – это что-то особенное. Он работает, и все. Где бы и когда бы вы ни использовали любой фрагмент его кода, кажется, что Эрик заранее прочитал ваши мысли. Все, что вам нужно, – на месте. Причем это подкреплено целым комплектом автоматизированных тестов компонентов.
Как у него это получилось? И почему у вас не получается?
Чувствуя себя все неудобнее, но сознавая, что без помощи не обойтись, вы наконец набираетесь смелости и идете к столу Эрика. «Как ты смог написать такой хороший, чистый код?»
«Да все просто, – отвечает он. – Сначала я пишу тесты».
14.1. Сначала пишем тестыРазработка на основе тестирования (test-driven development, TDD) – это техника разработки, в ходе которой применяются очень краткие рабочие циклы, обеспечивающие постепенное создание программы.
Вот как она строится.
1. Красный: прежде чем написать для системы любой новый код, пишется заведомо неуспешный тест компонента, демонстрирующий ваши намерения, которые вы собираетесь реализовать в новом коде. Здесь вы критически подходите к структуре кода.
2. Зеленый: затем вы делаете все, что необходимо для выполнения этого теста. Если вы уже видите всю реализацию целиком, добавьте новый код. Если нет – сделайте столько, сколько требуется для прохождения этого теста.
3. Рефакторинг: после этого вы возвращаетесь и подчищаете код, устраняя все погрешности, которые допустили, пытаясь выполнить тест. На этом этапе убираются дубли, и вы убеждаетесь, что код максимально экономичен, хорош и ясен.
Вы спрашиваете Эрика, как он узнает, что пора заканчивать с тестом. Эрик отвечает: «Я продолжаю писать тесты до тех пор, пока не убеждаюсь, что код выполняет все функции, упомянутые в пользовательской истории» (обычно это означает соответствие всем приемочным критериям данной истории).
Кроме того, Эрик руководствуется несколькими железными правилами, которые помогают ему не сбиться с пути.
Правило 1: не писать никакого нового кода, пока имеющийся код не пройдет созданный тест.
Правда, Эрик признает, что не следует этому правилу в 100 % случаев (некоторые вещи очень сложно протестировать заранее – таковы, например, пользовательские интерфейсы).
Но сущность метода, объясняет он, не в том, чтобы написать значительно больше кода, чем требуется. При написании теста мы неизбежно задумываемся о том, какую полезную нагрузку мы добавляем к программе, и не перегружаем программу техническими сложностями.
Правило 2: тестировать все, что может сломаться.
Следовать этому правилу не означает в буквальном смысле тестировать все, ведь на это может уйти целая вечность. Ключевое слово здесь – «может». Если существует вероятный шанс того, что что-то может сломаться, или мы хотим продемонстрировать, как программа будет действовать в определенных условиях, для такой ситуации нужно написать тест. Затем Эрик показывает пример того, над чем он сейчас работает.
«Знаешь, тут в Вегасе у нас есть несколько по-настоящему отчаянных игроков, – объясняет Эрик. – Наши ребята из отдела по обслуживанию хранилища данных хотят смоделировать самых влиятельных игроков. Они выясняют, что им нравится, а что нет, какие блюда и напитки они заказывают, а также все остальные детали, которые помогли бы завлечь этих людей в казино».
«Вот у нас в системе уже есть объекты с профилями определенных клиентов. Нужно выяснить, как сохранить такую информацию о профиле в базе данных».
Первым делом Эрик пишет тест. Здесь он предполагает, что код, который требуется протестировать, уже существует, и создает тест просто для того, чтобы самостоятельно убедиться, что этот код работает[22]22
Ссылка для скачивания: http://media.pragprog.com/titles/jtrap/code/tdd/test/CustomerProfileManagerTest.cs.
[Закрыть]:
[Test]
public void Create_Customer_Profile()
{
// настройка
var manager = new CustomerProfileManager();
// создание нового профиля клиента
var profile = new CustomerProfile(«Scotty McLaren», «Hagis»);
// подтверждение того, что в базе данных еще нет такого профиля
Assert.IsFalse(manager.Exists(profile.Id));
// добавление этого профиля
int uniqueId = manager.Add(profile); // получение id из базы данных
profile.Id = uniqueId;
// подтверждение того, что профиль был добавлен
Assert.IsTrue(manager.Exists(uniqueId));
// очистка
manager.Remove(uniqueId);
}
Эрик уверен, что тест позволит судить о том, можно ли спокойно добавить профиль нового пользователя. Поэтому он ненадолго изменяет подход и начинает думать, как добиться выполнения данного теста.
Здесь ясно видно, что нужно для этого сделать (взять информацию о профиле пользователя и сохранить ее в базе данных). Эрик продолжает работать и добавляет новые функции[23]23
Ссылка для скачивания: http://media.pragprog.com/titles/jtrap/code/tdd/src/CustomerProfileManager.cs.
[Закрыть].
public class CustomerProfileManager
{
public int Add(CustomerProfile profile)
{
// предположим, этот код сохраняет профиль // в базе данных, а потом возвращает его реальный id
return 0;
}
public bool Exists(int id)
{
// код, проверяющий существование клиента
}
public void Remove(int id)
{
// код, удаляющий клиента из базы данных
}
}
}
Теперь Эрик запускает тест и видит, что он выполняется. Ура!
Рефакторинг – это последний этап разработки на основе тестирования. Программист возвращается и все перепроверяет (код теста, код команд, файлы конфигурации и все остальное, необходимое для выполнения теста), после чего проводит серьезный рефакторинг (см. главу 13).
После рефакторинга Эрик вновь возвращается к самому началу и задает себе вопрос: протестировал ли он все, что может сломаться? Одно из требований этой истории предписывает убедиться, что в коде нет никаких дублей.
Итак, программист повторяет тот же процесс. Пишет неуспешный тест, делает все, чтобы он стал успешным, а затем проводит рефакторинг.
Иногда приходится сталкиваться с проблемой вроде «что было раньше: курица или яйцо?» (прежде чем проверить, работает ли вставка, нужен код, проверяющий, существует ли уже в системе профиль нужного пользователя).
В такой ситуации Эрик просто приостанавливает текущий тест, добавляет новые функции (разумеется, после тестирования), а затем возвращается к работе, которой занимался ранее.
Вы благодарите Эрика за то, что он познакомил вас с разработкой на основе тестирования, и возвращаетесь на рабочее место, размышляя о тестах, рефакторинге и написании кода.
В чем суть предыдущего примера?
Давайте остановимся на минуту и подумаем, что сейчас произошло и почему это так важно.
При разработке на основе тестирования мы сначала пишем тесты, а потом пытаемся их выполнить. Кажется, что мы действуем в обратном порядке. Разумеется, в школе нас учили поступать иначе.
Но задумайтесь об этом ненадолго. Есть ли лучший способ спроектировать программу, чем представить себе, что она уже существует!
Именно этим мы и занимаемся при обработке на основе тестирования. Программист пишет необходимый код так, как если бы он уже существовал, а потом проводит тесты, удостоверяющие, что код работает. Это отличный способ гарантировать, что вы напишете только те функции, которые вам нужны, и при этом убедитесь, что все они работают.
И еще – не расстраивайтесь, если команда не может сразу адаптироваться к разработке на основе тестирования. Это довольно продвинутая технология, которая базируется на тестировании компонентов и рефакторинге. И, если честно, возможны такие случаи, когда разработка на основе тестирования неприменима и вам придется просто сидеть и думать, в чем заключается проблема.
Но, когда вы приобретете некоторый опыт и поймете, какой ритм и потенциал придает работе процесс написания самого маленького теста, который затем нужно пройти и переработать, вам обязательно понравится, как выглядит и тестируется ваш код.
14.2. Использование тестов для разрешения сложных ситуацийПри написании кода разработчик сталкивается с многочисленными сложностями. Посмотрите, сколько решений пришлось принять Эрику, пока он наполнял программный интерфейс своего приложения «Создание пользовательского профиля».
Пересчитайте. Перед вами шесть решений, компромиссов и развилок, о которых должен подумать разработчик, – и все это в одной строке кода! Неудивительно, что иногда от внимания разработчика что-то ускользает.
Разработка на основе тестирования помогает справиться с солидным количеством сложных случаев, встречающихся вашей команде каждый день. Вы пишете тесты и убеждаетесь, что существует неуспешный тест, еще до того, как добавляете код в программу.
Кроме того, разработка на основе тестирования обеспечивает надежность процесса проектирования. Сосредотачиваясь на единственном тесте и добиваясь его выполнения, вы избавляетесь от необходимости думать о сотне вещей сразу.
Можно заняться одной небольшой проблемой, постепенно разобраться с тем, как лучше всего ее решить, и немедленно получить ответ на вопрос, помогающий понять, в правильном ли направлении вы движетесь.
Есть и другие причины начинать работу с тестов.
Благодаря этому становится гораздо проще поддерживать и модифицировать базу кода. Меньше кода – проще программа. А простой дизайн программы облегчает изменения.
Но хватит разговоров. Давайте проведем пару тестов и посмотрим, как работать на таких испытаниях.
Эрик приглашает вас написать с ним на пару код, который сравнивает старшинство двух карт. Он считает, что функции должны выполняться в классе Card, и поможет вам составить тест.
Напишите выбранное вами имя метода в классе Card. Этот метод должен сравнивать две карты и определять, какая из них старше.
Допустим, программа выглядит так[24]24
Ссылка для скачивания: http://media.pragprog.com/titles/jtrap/code/tdd/test/CardTest.cs.
[Закрыть]:
[Test]
public void Compare_value_of_two_cards()
{
Card twoOfClubs = Card.TWO_OF_CLUBS;
Card threeOfDiamonds = Card.THREE_OF_DIAMONDS;
Assert.IsTrue(twoOfClubs.IsLessThan(threeOfDiamonds));
}
Эрик вручает вам клавиатуру и спрашивает, что бы вы сделали, чтобы тест удалось пройти. У вас получается нечто следующее[25]25
Ссылка для скачивания: http://media.pragprog.com/titles/jtrap/code/tdd/src/Card.cs.
[Закрыть]:
public bool IsLessThan(Card newCard)
{
int thisCardValue = value;
int newCardValue = newCard.value;
return thisCardValue < newCardValue;
}
После того как тест пройден, Эрик спрашивает, не хотите ли вы провести здесь рефакторинг. Вы вносите изменения. Когда тест и метод доработаны, ваш код приобретает вид, показанный ниже.
Код из файла CardTest.cs:
[Test]
public void Compare_value_of_two_card()
{
Assert.IsTrue(Card.TWO_OF_CLUBS.IsLessThan(Card.THREE_OF_DIAMONDS));
}
Код из файла Card.cs:
public bool IsLessThan(Card newCard)
{
return value < newCard.value;
}
Когда этап цикла разработки на основе тестирования закончен, Эрик улыбается и говорит: «Думаю, ты все понял!» Вы, полный решимости испробовать такие методы на собственном коде, благодарите Эрика и отправляетесь на рабочее место, чтобы самостоятельно написать несколько тестов.
Где подробнее изучить эту тему
Чтобы прочувствовать смысл разработки на основе тестирования, рекомендую прочитать книгу Кеннета Бека Test Driven Development: By Example [Bec02], в которой дается немало хороших советов и объясняются более глубокие механизмы разработки на основе тестирования. Из нее вы также узнаете, как задействовать эти механизмы себе на пользу.
УЧЕНИК: Мастер, я не совсем понимаю разработку на основе тестирования. Как мне писать тесты для проверки кода, который еще не существует?
МАСТЕР: Пиши тесты так, как если бы необходимый код уже был у тебя в распоряжении.
УЧЕНИК: Но как мне узнать, что именно нужно тестировать?
МАСТЕР: Тестируй то, что требуется.
УЧЕНИК: То есть я должен просто писать тесты для проверки необходимых компонентов, и все, что нужно, появится в системе как по волшебству?
МАСТЕР: Да.
УЧЕНИК: Можно ли поподробнее узнать о том, как именно происходит вся эта магия?
МАСТЕР: Нет никакой магии. Ты просто выражаешь свои потребности в виде тестов. Создавая код таким образом, ты гарантируешь, что напишешь только то, что действительно требуется. Ты пользуешься тестами как инструментом, помогающим осознать твои намерения. Вот почему разработку на основе тестирования часто относят к методам проектирования, а не тестирования.
УЧЕНИК: То есть разработка через тестирование действительно касается больше проектирования, чем тестирования?
МАСТЕР: Это чрезмерное упрощение. Тестирование – основная часть данного метода, поскольку именно с помощью тестов мы доказываем, что создаваемый нами код работает. Но мы не можем закончить тестирование, не выполнив предварительного проектирования и не отразив наших намерений в коде.
УЧЕНИК: Спасибо, Мастер. Мне нужно подумать об этом на досуге.
Что дальше?
Вы уже чувствуете, как выстраиваются практические навыки? Тестирование компонентов позволяет удостовериться, что создаваемые нами элементы функционируют. Рефакторинг помогает не усложнять код. А разработка на основе тестирования представляет собой мощный инструмент, помогающий справиться со сложностью проектирования.
Осталось только собрать все вместе и убедиться, что ваш проект полностью готов к практической эксплуатации.
А теперь перейдем к заключительной главе и испытаем силу непрерывной интеграции!
Глава 15
Непрерывная интеграция: обеспечение готовности к работе
Приготовьтесь выдать результат, пригодный для практического использования. Научившись непрерывной интеграции своих программ, вы будете избавляться от ошибок практически сразу после их появления, стоимость внесения изменений в программу уменьшится и вы сможете уверенно ее внедрять.
Приступать к изучению непрерывной интеграции нужно прямо сейчас!
15.1. ПрезентацияНачнем с хорошего. Директор приглашает нескольких влиятельных инвесторов и хочет продемонстрировать им последнюю версию вашего флагманского продукта – симулятора игры блек-джек. А теперь плохая новость: гости прибудут через час!
Таким образом, у вас меньше 60 минут на то, чтобы создать стабильную сборку, отправить ее на тестовый сервер и подготовить к демонстрации.
Что делать?
Прежде чем ответить на этот вопрос, обдумайте, что может пойти не так при развертывании программы.
Это неприятности, которых требуется избежать либо хотя бы свести к минимуму в процессе непрерывной интеграции программы. Нам требуется развить культуру управления подготовкой продукта к производству и умение продемонстрировать программу кому угодно, когда угодно и где угодно.
Давайте рассмотрим два способа добиться этого.
Сценарий 1. Аврал
Один час! Прямо скажем, времени в обрез. Вы подаете сигнал тревоги, собираете команду и начинаете задавать вопросы со скоростью пулемета.
♦ У кого новейшая сборка?
♦ Чья настольная сборка самая стабильная?
♦ Кто может максимально быстро представить пример и запустить его?
Как известно, если хочешь сделать что-то хорошо – сделай это сам. Вы сообщаете команде, что интеграция будет проходить на вашей машине и у всех есть 15 минут, чтобы вставить сделанные изменения в вашу ветку кода.
Начинается интеграция кода, и возникают новые проблемы. Интерфейсы в основных классах изменились. Файлы конфигурации были модифицированы. Файлы из старой системы претерпели рефакторинг, и в них явно чего-то не хватает. Сведение всех элементов воедино сразу превращается в кошмар.
Проклиная про себя директора за то, что он не дал вам достаточно времени на подготовку, вы приказываете команде откомментировать и исключить все, что мешает осуществить интеграцию.
Затем, в последние 5 минут вдруг появляется свет в конце тоннеля – пошла компиляция!
Но надежда сменяется катастрофой – инвесторы явились на 5 минут раньше срока. На тестирование нет времени.
Вы скрещиваете пальцы и развертываете программу, запускаете демонстрационную версию и… она отказывается работать. Вы быстро устраняете проблему и запускаете программу, но она «падает» еще раз, как только вы проходите вводную заставку.
Директор в некотором замешательстве вынужден признать, что демоверсия пока не работает. Тогда он спрашивает, не может ли команда показать вместо программы демонстрационные модели.
Сценарий 2. Ничего особенного
Вы знаете, что до демонстрации у вас есть еще целый час, поэтому сообщаете команде, что по истечении этого времени работу предстоит предъявить инвестору. Вы говорите, что были бы очень благодарны, если бы все успели быстренько закруглиться и проверить то, что уже готово.
После того как все сохранили сделанное, вы проверяете последнюю версию кода, проводите тесты, убеждаетесь, что все работает, и отправляете программу на тестовый сервер. На все уходит не более 5 минут.
Инвесторы прибывают раньше времени. Демоверсия работает отлично. А ваш босс благодарит вас за то, что вы, практически не имея времени на подготовку, смогли показать такой класс, и вручает вам что-то, о чем вы давно мечтали, – например, ключи от VIP-санузла. Ладно, допустим, вам сто лет не нужен этот санузел, но идею вы, полагаю, поняли.
Подготовка к презентации и подготовка кода к реальному использованию – не обязательно нервозное, трудоемкое и выводящее из себя событие.
Нужно, чтобы сборка, интеграция и развертывание вашей программы не были чем-то из ряда вон выходящим. А для этого необходимо наладить качественный и гладкий процесс интеграции и воспитать культуру обеспечения готовности продукта к производству.
15.2. Культура подготовки продукта к производствуВ экстремальном программировании есть выражение, гласящее, что производство начинается с первого дня проекта. С того момента, как вы запишете первую строку кода, начинается подготовка проекта к реальному использованию, и все, что вы делаете, – просто вносите изменения в живую систему.
Из этого вытекает совершенно нетипичный подход к коду. Вы воспринимаете производство и внедрение не как события, относящиеся к какому-то отдаленному будущему, а понимаете, что уже здесь и сейчас вы и команда занимаетесь производством и это требует от вас необходимого уровня ответственности.
Сторонникам гибкой разработки нравится такое отношение к производству, так как оно учитывает, что для производства программы нужно гораздо больше, чем просто написать код, и прививает командам ощущение, что все изменения вносятся в систему, подготавливаемую к реальному использованию.
Правда, развитие культуры обеспечения готовности продукта к производству требует определенных усилий. В частности, такая культура невозможна без строгой дисциплины, а соблазн повременить с подготовкой кода к реальному использованию, но зато соблюсти сроки, может быть очень велик.
Но те, кто начинает готовиться к выпуску заранее, могут при реализации проектов буквально «развернуться на пятачке». Развертывание проходит легко, изменения в систему вносятся регулярно и уверенно, а реагировать на потребности клиента удается быстрее, чем конкурентам.
В развитии такой культуры может помочь непрерывная интеграция.
15.3. Что такое непрерывная интеграцияНепрерывная интеграция – это процесс постоянного учета изменений, которые разработчики вносят в программу, и интеграция этих изменений в единое целое изо дня в день.
Данный процесс можно сравнить с написанием книги. Предположим, что вы с соавтором работаете вместе над главой и вам нужно согласовывать изменения. Неплохо было бы объединить несколько простых фрагментов, в которых изменяется пара фраз.
Если достаточно долго не сводить варианты воедино, могут начаться проблемы.
При написании программ возникает аналогичная ситуация. Чем дольше вы работаете, не объединяя сделанные изменения с программами коллег, тем сложнее будет впоследствии интегрировать все созданные компоненты воедино.
Давайте посмотрим, как это происходит на практике.
Работать надо быстро
Однажды я работал над проектом, в рамках которого нужно было создать крупный инструмент для автоматизированного написания тестов записи и воспроизведения. На самом деле инструмент был отличный, все стали писать с его помощью свои тесты, а о низкоуровневых и быстрых тестах компонентов просто забыли.
Некоторое время все шло хорошо. Но по мере накопления тестов, созданных автоматически, время сборки постепенно выросло с 10 минут до более чем трех часов.
Нас это просто погубило. Люди прекратили пользоваться сборкой. Они начали пренебрегать тестированием, и неисправные сборки стали в нашем проекте нормой.
Не повторяйте нашей ошибки и не позволяйте сборкам вырастать до огромных размеров. Сборка должна занимать не более 10 минут – это железное правило. В некрупных проектах удается добиться, чтобы время сборки не превышало 5 минут.
Правообладателям!
Это произведение, предположительно, находится в статусе 'public domain'. Если это не так и размещение материала нарушает чьи-либо права, то сообщите нам об этом.