Непрерывная интеграция

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

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

Этот подход достигается не дорогими и сложными средствами, его сущность лежит в использовании простого правила для всей команды: хотя бы раз в день вносить сделанные изменения в репозиторий исходного кода.

Описав эту практику людям, я обычно получаю 2 возражения: “здесь это работать не будет” и “если мы так будем делать – большой разницы не будет”. Что люди понимают после того, как попробуют, так это то, что, во-первых, это значительно проще, чем выглядит на первый взгляд, во-вторых – существенно изменяет тактику разработки. Таким образом, третий типичный ответ – “да, мы так и делаем – как вы могли без этого жить?”

Термин “непрерывная интеграция” появился в процессе разработки под названием “Экстремальное программирование”, как один из его оригинальных двенадцати практик. Когда я начал работать в ThoughtWorks в качестве консультанта, я поддержал использование этой техники в проекте, в котором принимал участие. Мэтью Фоммель воплотил мои смутные убеждения в реальное действие и мы увидели, что проект перешел от редких и сложных интеграционных фаз к полному их отсутствию. Мэтью и я записали наш опыт в оригинальной версии данной статьи, которая была одной из наиболее популярных на моем сайте.

Хотя практика непрерывной интеграции не требует особых средств, мы посчитали полезным использовать так называемый “сервер непрерывной интеграции”. Наиболее известный в данный момент из таковых – CruiseControl – проект с открытыми исходными кодами, который начали разрабатывать несколько человек из ThoughtWorks, а теперь поддерживается большим сообществом. Оригинальная версия была написана на Java, но также доступна на платформе Microsoft в виде CruiseControl.NET.

Разработка новой функциональности с непрерывной интеграцией

Самый простой способ объяснить, что такое НИ и как она работает – это описать небольшой пример разработки новой функции (feature). Давайте представим, что мне нужно сделать что-то с небольшой частью программной системы. На данный момент не имеет значение, в чем конкретно заключается задание, главное, что оно небольшое и может быть выполнено за несколько часов (более длительные задания и другие вопросы мы рассмотрим ниже).

Я начинаю с извлечения копии текущего исходного кода из репозитария на свой компьютер с помощью системы контроля версий, выполняя команду “check out”.

Предыдущий параграф имеет смысл для людей, использующих систему контроля версий исходного кода, но не имеет смысла для всех остальных. Позвольте мне быстро описать, в чем ее суть. Система контроля версий хранит все исходные коды проекта в репозитории. Текущее состояние системы обычно характеризуется термином “mainline” (или HEAD – прим. пер.). В любое время разработчик может сделать контролируемую копию “mainline” на свою машину – это называется “checking out”. Копия разработчика называется “working copy” (рабочая копия). (В дальнейшем, основной операцией будет обновление рабочей копии до “mainline” – на практике это почти то же самое.)

Итак, у меня есть рабочая копия исходных текстов системы и я делаю все, чтобы выполнить задание. Точнее, выполнение задание заключается в изменении существующего работающего кода и написании автоматизированных тестов. НИ подразумевает высокую степень покрытия системы автоматизированными тестами, как средство самотестирования. Чаще всего, тесты построены на XUnit-фреймворках (семейство JUnit, NUnit, DBUnit, итп. – прим. пер.).

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

После успешной сборки, можно подумать о том, чтобы сохранить (commit) сделанные изменения в репозиторий. Есть, конечно, загвоздка в том, что другие разработчики к этому моменту уже внесли свои изменения в репозиторий (чаще всего именно так и происходит). Я обновляю код из репозитария, собираю и тестирую проект еще раз, что может привести как к ошибке компиляции, так и к заваленным тестам. В этом случае, исправление этих ошибок лежит в моей области ответственности. Этот процесс повторяется до тех пор, пока я не получаю полностью работающую сборку, синхронизированную с репозиторием, после чего можно записать свои изменения в репозиторий.

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

Если между двумя разработчиками происходит конфликт (например, одновременное изменение одних и тех же строк в одном файле – прим. пер.), он обычно обнаруживается последним разработчиком во время сборки после обновления рабочей копии. Либо во время интеграционной сборки на сервере. В любом случае, ошибка быстро обнаруживается. В этом случае, наиболее важным заданием является ее исправление, чтобы снова добиться успешной сборки. В среде непрерывной интеграции никогда нельзя держать интеграционную сборку в состоянии ошибки долгое время. В хорошей команде должно быть много успешных сборок в день; плохие сборки могут появляться время от времени, но должны быстро исправляться.

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

Практика непрерывной интеграции

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

Поддержка единого репозитория исходных кодов проекта

Проекты разработки информационных систем содержат большое количество файлов, которые требуют организации для сборки продукта. Отслеживание всех файлов требует больших усилий, особенно, если над ними работают одновременно несколько человек. Таким образом, не будет сюрпризом, если я скажу, что на протяжении многих лет команды разработчиков сделали много систем для такого рода задач. Эти средства: системы управления исходным кодом, управление конфигурациями, системы контроля версий, репозиториями, и множество других наименований – являются неотъемлемой частью большинства проектов. На самом деле, очень удивительно и грустно, что не всех. Очень редко, но мне встречалось работать в проектах, не использующих подобные системы, а использующих жуткую комбинацию локальных и сетевых дисков.

Итак, вам стоит убедиться, что используется приличная система контроля версий. Цена в данном случае не является проблемой, так как на рынке существуют качественные системы с открытыми исходными кодами. Лучшим средством на сегодняшний день из бесплатных средств является Subversion. (Более старая система CVS все еще широко используется и лучше, чем ничего, но Subversion – более современна.) Интересен факт, что многие разработчики Subversion предпочитают многим платным системам. Единственное средство, стоящее своих денег, о котором я слышал – это Perforce.

Как только вы установили систему контроля версий, убедитесь, что всем известно, где искать проектные файлы. Не должно возникать вопроса “Где найти такой-то файл?” – ответ будет всегда один – в репозитории.

Хотя многие команды используют репозиторий, общей ошибкой является то, что они не кладут туда все файлы. Кроме исходного кода, там должны лежать: тестовые скрипты, файлы настроек, схемы баз данных, установочные скрипты и сторонние библиотеки.
Случалось, что даже компилятор C++ записывали в репозиторий (что было важно во времена разношерстных С++ компиляторов). Основное правило заключается в том, чтобы вы смогли сесть за чистую машину, сделать checkout и полностью собрать и запустить проект. На этой машине должно быть минимальное количество необходимых разработчику вещей (обычно только большие, сложные для установки и стабильные). Например, операционная система, среда разработки Java или система управления базой данных (СУБД).

Вы должны положить в систему контроля версий все необходимое для сборки проектов, также вы можете использовать репозиторий для хранения других файлов, связанных с проектом. Файлы проектов IDE (среды разработки – прим. пер.) обычно удобно хранить там, так как это наиболее простой способ разделить их с другими разработчиками.

Одна из функций системы контроля версий заключается в возможности создавать несколько веток (branches) для поддержки нескольких направлений разработки проекта. Эта полезная, даже обязательная функция часто используется слишком интенсивно и приводит к большим неудобствам для разработчиков. Используйте ветки по минимуму, в частности, используйте “mainstream” (обычно ее называют trunk – прим. пер.) как единую ветку для разработки. Большую часть времени и работы должно выполняться в этой ветке (другие ветки обычно создаются для исправления ошибок перед развертыванием приложения в рабочем режиме (production) и для временных экспериментов).

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

Автоматизация сборки

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

Средства автоматической сборки – обычная часть среды разработки. Мир Unix-разработчиков десятилетия пользовался утилитой make, сообщество Java разработало Ant, сообщество .NET – NAnt, и в настоящее время имеет MSBuild. Убедитесь, что вы можете собирать и запускать систему используя эти скрипты одной командой.

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

Скрипты сборки могут быть различных видов, часто специализированные под платформу или сообщество, но не обязательно. Хотя большинство наших Java-проектов использует Ant, некоторые использовали Ruby (система Ruby Rake очень приятное средство сборки). Также Ant нам очень пригодился для сборки проекта на Microsoft COM.

Полная сборка может занять довольно продолжительное время, хотя вы не должны выполнять все шаги, если были сделаны незначительные изменения. Хорошая система сборки должна анализировать сделанные изменения. Общепринятый способ – сравнивать даты изменения исходного файла и результата и выполнять операцию, только если исходный файл новее. Зависимости определяются сложнее – требуется определить, не изменился ли хотя бы один файл, который требуется для сборки модуля, если да – то пересобрать модуль. Компиляторы могут отслеживать, а могут и не отслеживать такого рода зависимости.

В зависимости от того, что вам нужно, вы можете выбирать, что именно вы хотите собрать. Вы можете собрать систему с тестами, либо без тестов, или с различными наборами тестов. Некоторые компоненты могут собираться отдельно. Скрипт сборки должен позволять запускать различные цели (targets) для различных случаев.

Большинство из нас используют IDE (среды разработки), и большинство сред имеют какую-либо встроенную систему сборки. Однако эти системы обычно специфичны для конкретной среды и часто ограничены по функциональности. Кроме того, они требуют, чтобы среда разработки был запущена. Это нормально для пользователей, работающих в IDE и использующих ее для разработки. Однако очевидно, что должен быть основной скрипт, который может быть запущен на сервере, а также была возможность запустить его из другого скрипта. Таким образом, мы не против, что Java разработчики собирают проект с помощью среды разработки, но основной скрипт сборки использует Ant, чтобы гарантировать то, что проект может быть собран на сервере.

Сделайте свою сборку самотестируемой

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

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

Мои постоянные читатели знают, что я большой поклонник и XP и TDD, но ни один из этих подходов не обязателен для того, чтобы получить все преимущества от самотестирования кода. Оба этих подхода предполагают написание тестов для кода до разработки самого кода – в этом режиме тесты нужны для разработки архитектуры системы, в тоже время их предназначение – обнаружение ошибок. Это отличная практика, но не обязательна для целей непрерывной интеграции, где у нас более слабые требования к самотестируемому коду. (Хотя я предпочитаю TDD для создания самотестируемого кода.)

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

За последние несколько лет подъема TDD сделало популярными семейство фреймворков с открытыми исходными кодами XUnit, идеальными для такого рода тестирования. Средства XUnit доказали свою ценность для нас в компании ThoughWorks и я всегда советую людям их использовать. Изначально разработанные Кентом Бэком (Kent Back), эти средства позволяют очень просто полностью настроить среду для самотестирования кода.

Средства XUnit, конечно, являются отправной точкой для разработки тестов, но также следует обратить внимание на другие средства, позволяющие разрабатывать более широкий спектр тестов. В настоящее время существует довольно большой выбор, например, FIT, Selenium, Sahi, Warir, FITnesse и множество других, для которых я не буду приводить полный перечень.

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

Все сохраняют изменения в репозитории каждый день

Интеграция в первую очередь связана с коммуникациями. Интеграция позволяет разработчикам сообщить другим разработчикам о своих изменениях. Частые коммуникации позволяют людям узнавать о новых изменениях, как только они появились.

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

Часто выполняя эту процедуру, разработчики быстро обнаруживают конфликты между собой. Ключ к быстрому решению проблем – в их быстром обнаружении. Если разработчики заносят свои изменения каждые несколько часов, конфликт обнаруживается быстро, и также просто разрешается, так как к этому моменту версии кода еще не сильно разошлись. Конфликты, которые не обнаруживаются в течение недель, разрешаются очень сложно.

Фактически, то, что вы собираете проект после обновления рабочей копии, означает, что вместе с текстовыми ошибками, вы также определяете ошибки компиляции. Если код покрыт тестами, вы также обнаруживаете ошибки в работе кода. Последний вид конфликтов позволяет избавиться от особенно трудных для обнаружения ошибок, которые могут оставаться незамеченными долгое время. Так как между изменениями проходит всего лишь несколько часов работы – мест, где могут быть проблемы не так много. К тому же, так как самих изменений не так много, вы можете использовать отладку по разнице между файлами (diff) для определения места ошибки.

Мое основное правило заключается в том, что каждый разработчик должен фиксировать свои изменения в репозитории каждый день. На практике даже чаще полезней делать это несколько раз в день. Чем чаще вы фиксируете свои изменения – тем меньше у вас будет конфликтов и проще будет разрешить те, что есть.

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

Каждое изменение в репозитории должно включаться в сборку на интеграционном сервере

При ежедневной фиксации изменений, достигается частое выполнение тестов в проекте. Это означает то, что код в репозитории находится в жизнеспособном состоянии. Однако на практике дела могут обстоять не так хорошо. Одна из причин заключается в том, что разработчики не делают обновление и сборку перед фиксацией изменений. Источником другой является различия в настройках окружения в компьютерах разработчиков.

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

Чтобы этого добиться, существуют 2 пути: использовать ручную сборку, либо сервер непрерывной интеграции.

Подход с ручной сборкой описывается наиболее просто. Очевидно, это примерно то же самое, что и сборка на локальной машине, которую разработчик делает перед фиксацией изменений. Разработчик идет к интеграционному серверу, обновляет там рабочую копию (в которую попадают его последние изменения из репозитория) и запускает интеграционную сборку. Он следит за процессом сборки, и как только сборка прошла успешно – его работа выполнена. (См. также описание Джима Шора.)

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

Мы в компании ThoughtWorks, как горячие поклонники серверов непрерывной интеграции, ведем разработку CruiseControl и CruiseControl.NET – широко используемых серверов НИ с открытыми исходными кодами. Такие ThoughtWorker-ы, как Пол Джулиус, Джейсон Ип и Оуен Роджерс все еще активно участвуют в разработке этих проектов. Мы используем CruiseControl почти на любом проекте, который делаем и всегда были довольны результатом.

Но не все предпочитают использовать сервер НИ. Джим Шор дает вполне аргументированное описание того, почему он предпочитает ручной подход. Я согласен с ним, что НИ значительно больше, чем просто установка CruiseControl. Чтобы подход НИ эффективно работал – должны использоваться все необходимые практики. Но, на самом деле, многие команды, успешно работающие по НИ находят CruiseControl полезным средством.

Часто в проекте выполняются регулярные сборки по расписанию, например, каждую ночь. Это не то же самое, что непрерывная сборка и этого недостаточно для непрерывной интеграции. Основной целью НИ является обнаружение проблем настолько быстро, насколько это возможно. Ночные сборки (nightly builds) означают, что ошибки останутся невыявленными целый день, пока кто-нибудь их не обнаружит. Так как они находятся в системе продолжительное время, их поиск устранение также займет большее время.

Ключевым моментом в непрерывной интеграции является то, что если сборка прошла неуспешно, она должна быть немедленно исправлена. Аксиомой при работе с НИ является то, что вы всегда работаете со стабильным кодом. Ничего страшного в том, что сборка на сервере все-таки не прошла, хотя если это случается все время, стоит рекомендовать людям быть более внимательными при обновлении и локальной сборке перед фиксацией изменений. Однако когда сборка на сервере все же провалилась, очень важно исправить это как можно быстрее.

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

Делайте сборку быстрой

Основная цель непрерывной интеграции – обеспечить быструю обратную связь. Ничто так не подрывает идею НИ, как долго выполняющаяся сборка. Здесь я вынужден согласиться с очевидным вопросом, что должно считаться “долгой” сборкой. Большинство моих коллег считают сборку, продолжающуюся целый час полностью неоправданной. Я помню команды, мечтающие о том, чтобы ускорить сборку, иногда и мы попадаем в ситуации, когда очень сложно добиться приемлемой скорости сборки.

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

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

Возможно, ключевым моментом здесь является настройка поэтапной сборки. Идея поэтапной сборки (также известной как “последовательная сборка” - build pipeline) заключается в том, что несколько сборок выполняется последовательно. Фиксация изменений вызывает первую сборку (commit build). Она должна выполняться быстро и поэтому мы должны ее сократить, что должно уменьшить ее способность обнаруживать ошибки. Трюк заключается в том, чтобы соблюдать баланс между требованиями по обнаружению ошибок и скоростью, чтобы первая сборка (commit build) была достаточна для того, чтобы люди могли продолжать работать.

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

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

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

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

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

Производите тестирование в копии среды реальной эксплуатации

Цель тестирования – выявить любую проблему, которая может проявиться в среде эксплуатации (production environment) в контролируемых условиях. Значительную роль в этом играет среда выполнения, которая используется в production. В случае тестирования в другой среде – каждое отличие оборачивается риском, того, что то, что случится в среде тестирования, никогда не проявится в среде эксплуатации.

В результате, вы хотите настроить среду тестирования настолько ближе к среде работы приложения, насколько это возможно. Используйте те же самые СУБД, тех же версий, ту же версию операционной системы. Положите все необходимые библиотеки из рабочей среды в тестовую, даже если система их не использует. Используйте те же IP-адрес и порты, запускайте систему на том же оборудовании.

Однако в реальности существуют ограничения. Если вы пишите настольное ПО, практически нереально его протестировать на каждом возможном настольном компьютере со всеми вариантами установленного ПО других фирм, которое может использоваться. Также среда эксплуатации системы может быть неоправданно дорогой для клонирования в тестовых целях (хотя я часто наблюдал случаи “ложной” экономии для систем средней стоимости). Несмотря на все эти ограничения, вашей целью должно стать копирование среды эксплуатации настолько точно, насколько вы можете и понимание рисков тех отличий, которые все же остались.

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

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

Простой доступ к файлам последней сборки

Один из наиболее сложных моментов в разработке программного обеспечения заключается в том, чтобы быть уверенным, что вы делаете именно то, что нужно пользователям. Очень трудно заранее специфицировать, что именно нужно сделать и не ошибиться в этом; часто людям удобнее посмотреть на то, что не совсем правильно, и сказать, что конкретно нужно исправить. Гибкие (Agile) процессы разработки явно рассчитывают на эту особенность человеческого поведения.

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

Сделать это достаточно просто: нужно убедиться, что есть место, где лежат последняя сборка и о котором все знают. Также может быть полезным выкладывать туда несколько последних сборок. Для самой последней сборки вы должны быть уверены в том, что она прошла тестирование на достаточно “сильном” наборе тестов.

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

Все должны знать что происходит

Как уже упоминалось, НИ тесно связана с коммуникациями в проекте. Вы должны быть уверены в том, что все знают, где посмотреть текущее состояние системы и изменения, которые в ней появились.

Одной из наиболее важных вещей является состояние основной сборки. Если вы используете CruiseControl, то вам доступен web-сайт, на котором видно собирается ли проект в данный момент и каков статус последней сборки. Во многих командах любят использовать более привлекательные средства для непрерывной индикации состояния сборки, например, зеленая лампочка, когда сборка проходит, и красная – если сборка не прошла. Особенно часто используются красные и зеленые лавовые лампы, которые не только отображают, что сборка не прошла, но и время, которое она была в этом состоянии. По пузырькам на красной лампе можно определить, что сборка была сломана слишком долго. В каждой команде обычно делают собственный выбор относительно сенсоров сборки – все ограничивает только фантазией! (Недавно я видел эксперименты с танцующим кроликом.)

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

Конечно, веб-страница на сервере непрерывной интеграции может содержать значительно больше полезной информации. CruiseControl отображает информацию не только о том, что в данный момент собирается, но и какие изменения были сделаны. Cruise также хранит историю изменений, позволяя команде следить за недавними активностями в проекте. Я встречал руководителей команд, которые использовали эту информацию для того, чтобы знать, кто какие изменения делает в системе.

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

Хорошим информационным дисплеем может быть не только экран компьютера. Приведу один пример из проекта, в который внедрялась непрерывная интеграция. На протяжении долгого времени не удавалось делать стабильные сборки. Тогда мы повесили на стену календарь на целый год и каждый день QA -группа отмечала зеленым стикером дни, когда сборка проходила основные тесты, и красным – если нет. Со временем календарь показал устойчивое улучшение процесса сборки, когда зеленые квадраты стали обычным явлением – календарь сняли. Его задача была выполнена.

Автоматическое развертывание

Чтобы использовать непрерывную интеграцию, вам необходимы несколько сред выполнения (environment): одна, чтобы исполнять основной набор тестов (commit tests) и одна или более для запуска вторичных тестов. Так как вы перемещаете исполняемые файлы между этими средами по нескольку раз в день, конечно, вы захотите делать это автоматически. Таким образом, важно иметь скрипты, которые позволят вам просто разворачивать приложение в любую среду выполнения.

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

Если вы автоматически развертываете приложение в среде эксплуатации, нужно также предусмотреть и автоматический откат. Время от времени случается что-то ужасное, поэтому нужно иметь возможность быстро вернуться к последнему стабильному состоянию. Это также уменьшит напряжение при внедрении новой сборки, разработчики не будут бояться чаще развертывать новую сборку и, следовательно, чаще поставлять пользователям новые функции. (Сообщество ruby on rails разработало утилиту Capistrano, которая является хорошим примером такого рода вещей.)

См. статью Evolutionary Database Design

Миграция базы данных является общим препятствием для многих людей, которые выпускают частые релизы системы. Изменять базу данных неудобно, потому что недостаточно всего лишь изменить ее схему, намного сложнее убедится, что данные тоже были перенесены корректно. В этой статье описываются приемы, используемые моим коллегой Прамодом Сэдэлейдж для автоматического рефакторинга и миграции баз данных. Статья предшествовала изданию книги Pramod-а и Скотта Амблера по рефакторингу баз данных [ambler-sadalage].

С одним интересным вариантом автоматического развертывания я столкнулся при разработке одного общедоступного web-приложения. Идея заключалась в том, чтобы развертывать временную версию для некоторого подмножества пользователей. Это позволяет протестировать новые функции и пользовательский интерфейс до финального релиза. Автоматическое развертывание является частью дисциплины непрерывной интеграции и необходимо для полноценной работы.

Преимущества непрерывной интеграции

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

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

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

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

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

Ошибки обычно накапливаются. Чем больше ошибок, тем сложнее избавится от каждой. Частично из-за того, что они могут взаимодействовать друг с другом (когда симптом ошибки проявляется как следствие нескольких ошибок в коде, найти каждую значительно труднее). Есть и психологический фактор – сложнее находить и исправлять ошибки, когда их много – феномен, которые Pragmatic Programmers называют синдромом “Разбитых окон” (“Broken Windows”).

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

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

Знакомясь с непрерывной интеграцией

Итак, вам хочется попробовать Непрерывную Интеграцию – с чего можно начать? Полный набор практик, описанный выше, даст вам все преимущества подхода, но совершенно не обязательно использовать их все сразу.

Здесь нет единого рецепта на все случаи – все зависит от ваших установок и команды. Но есть необходимые условия, чтобы все заработало.

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

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

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

Если вы стартуете новый проект – начните сразу с непрерывной интеграции. Следите за временем сборки и принимайте меры как только вы выходите за порог в 10 минут. Быстрые действия позволят вам вовремя реструктуризировать код до того, как он вырастет настолько, что станет большой проблемой.

Все описанное выше, безусловно, вам поможет, но также стоит найти кого-то, кто использовал непрерывную интеграцию ранее. Как и любая другая техника - ее трудно представить, пока вы не знаете как выглядит конечный результат. Ментор может стоить денег, но вы можете потерять их больше в виде потраченного времени и производительности, если вы этого не сделаете. (Реклама: да, конечно, мы в компании ThoughtWorks выполняем консультации в этой области, Но, в конце концов, мы уже сделали большинство ошибок, которые только можно сделать.)

Заключение

С того времени, как мы с Мэтом написали первоначальный вариант этой статьи, Непрерывная интеграция стала основной техникой в разработке программного обеспечения. Едва ли один проект в ThoughWorks прошел без нее – и мы видим, что весь мир ее использует. Я также едва ли слышал плохое слово о непрерывной интеграции, в отличие от других более спорных практик Экстремального программирования.

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

Благодарности

В первую очередь и более всего – Кенту Беку и многим моим коллегам с проекта the Chrysler Comprehensive Compensation (C3). Это был первый мой шанс увидеть непрерывную интеграцию на практике со значительным набором юнит-тестов. Это показало мне, что возможно и вдохновило на дальнейшие разработки в этой области на многие годы.

Спасибо Мэтту Фомеллю, Дэйву Райсу и всем, кто разрабатывал и поддерживал непрерывную интеграцию в Atlas. Этот проект был знаковым для непрерывной интеграции на большом масштабе и показал его преимущества для существующего проекта.

Пол Джулиус, Джейсон Ип, Оувен Роджерс, Майк Робертс и другие контрибьюторы в OpenSource проект CruiseControl. Хотя это средство не обязательное, многие находят его полезным. CruiseControl сыграл большую роль в популяризации и привлечении разработчиков к использованию непрерывной интеграции.

Одна из причин, по которой я работаю в ThoughtWorks – это доступ к хорошему набору практических проектов, над которыми работают талантливые люди. Почти каждый проект, который я наблюдал принес свою долю знаний в практику непрерывной интеграции.

Мартин Фаулер
Перевод с англ. – Евгений Гаврилов (eugavrilov@gmail.com)