Pull to refresh
129.29
Контур
Делаем сервисы для бизнеса

Как перестать беспокоиться и начать работать?

Reading time 9 min
Views 28K
В прошлый раз, когда мы рассказывали о работе нашей команды, многих интересовали подробности организации работы непосредственно разработчиков, о чём мы сейчас и расскажем. Не стоит ожидать «срывов покровов» и открытий, ведь всё, что делают разработчики ни раз описывалось и обсуждалось, но то, что мы делаем в совокупности в реальных крупных проектах, делается не так уж часто (честно говоря, я этого вообще больше нигде не видел). То есть ожидать-то не стоит, но «срывы покровов» произойдут :)

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

При правильном подходе разработка новой функциональности может происходить довольно быстро и фокус тут в том, как не сломать при этом то, что было сделано раньше или же быстро понять, что именно ты сломал, и быстро это исправить. Каждый раз тестировать все детали и нюансы вручную очень долго и неэффективно, ведь в Эльбе сегодня уже более 400 «экранов». И проблема не только в том, что тестеры должны все это проверить вручную — очень много времени уходит на сценарий «тестер добавил баг — разработчик исправил баг — тестер проверил и закрыл/переоткрыл баг». Можно долго рассуждать о том, как трудно сохранить быстрорастущую систему в стабильном состоянии, почему код превращается в гавно, почему страшно делать исправления и никто не берет на себя смелость сказать дату релиза даже с точностью до месяца (потому что это было бы безответственно), но гораздо интереснее узнать о том, как сделать так, чтобы было хорошо.

Тесты, тесты, тесты


Я написал слово «тесты» 3 раза, не потому, что повторять слова по три раза весело, а потому что мы используем 3 вида тестов:
  1. Модульные тесты бизнес-логики. Эльба — это совокупность нескольких взаимодействующих друг с другом сервисов (веб, хранилище данных, сервис справочников, сервис печати и т.д.) Все эти сервисы и серверную часть веба мы пишем в TDD стиле. Про TDD сказано и написано очень много. Основным плюсом этой методологии я считаю создание ситуации, в которой написание тестов — это не рутинная и не скучная работа. Постепенно наращивать тесты и фанатично писать кода ровно столько, чтобы текущий набор модульных тестов проходил, но ни строчкой больше — это очень весело, это такая ментальная игра, вроде шахмат. Кроме того, честно покрывая каждую строчку вашего кода модульными тестами, вы получаете более совершенный код. Тут уместен фанатизм: если возник код, который можно стереть и при этом не упало ни одного теста, этот код должен быть стерт немедленно, в воспитательных целях. Стоит сказать, что представление о хорошем коде у каждого свое: студенты считают совершенным код, который компилируется, а некоторые разработчики любой код, который работает. К счастью можно не спорить по этому поводу. Прекраснейший человек Uncle Bob давно обобщил все, что стоит обобщать в области формальных характеристик хорошего кода и написал серию статей об этом. Мы в Эльбе считаем хорошим тот код, который удовлетворяет принципам SOLID. Если вы до сих пор не знаете что это и при этом не являетесь Хаскелл-разработчиком, то есть вероятность, что вы живете неправильно или зря :) Код, покрытый модульными тестами, легко наращивать и модифицировать: тесты гарантируют неизменность старой бизнес-логики, а SOLIDность позволяет делать важные изменения сугубо локально (у вас ведь нет копипастов и 3 классов, которые делают одно и тоже?) — вы достигаете заявленной цели, скорость разработки не снижается и все происходит с заданным качеством.

  2. Модульные тесты для клиентского JavaScript. Серверная бизнес-логика это прекрасно, но конечные пользователи видят веб-приложение. И они хотят не просто наблюдать унылые страницы, все должно летать и взрываться. Поэтому мы пишем много клиентской логики. Тут нет ничего нового по сравнению с серверной бизнес-логикой: код должен быть SOLIDным и покрытым тестами, просто язык другой. Такими образом мы тестируем классы и контролы общего назначения (например, клиентские валидаторы).

  3. Функциональные тесты веб-приложения. Все модульные тесты могут проходить, но это вовсе не гарантирует, что у пользователя нет никаких проблем с конкретной страницей. Мы не тестируем модульными тестами заполнение полей каждой формы и обработку этих полей на сервере. Чтобы зафиксировать правильное поведение конечных страниц, мы используем функциональные тесты. Фактически мы повторяем поведение пользователя: кликаем на ссылку, ожидаем загрузку страницы с определенным набором полей, заполняем эти поля, проверяем корректность валидации, нажимаем на кнопку «сохранить» и убеждаемся, что именно то, что было вписано в поля, осело в нашем хранилище. Даже если на странице, в зависимости от каких-то условий, меняется какая-нибудь маленькая надпись — на это есть тест. Если разработчик говорит, что невозможно протестировать все условия, под которые он писал код, значит, он писал этот код пьяным или врёт (речь идёт об обычной бизнес-логике, а не о научных приложениях, например). Вот так понятно может выглядеть тест:

    [Test]
    public void ChangeEmailWithGoodPassword()
    {
           const string newEmail = "hello_habr@gmail.com";
     
           Prepare();
     
           emailSettingsPage.Email.TypeValueAndWait(newEmail);
           emailSettingsPage.Password.TypeValueAndWait("superpassword");
           emailSettingsPage.SaveButton.Click();
           emailSettingsPage.WaitSaveSucceded();
           emailSettingsPage = emailSettingsPage.Refresh();
           emailSettingsPage.Email.WaitValue(newEmail);
    }
    

    А вот что происходит при запуске теста:



    Чтобы писать такие понятные тесты, мы для них строим абстракции над страницами и контролами, благодаря которым сильно снижается сложность их поддержки, повышается читаемость и даже появляется возможность писать функциональные тесты по TDD!

Запуск тестов


Тесты пишутся для того, чтобы их запускать, поэтому очень важно, чтобы тесты запускались легко (одной кнопкой!), стартовали мгновенно и быстро отрабатывали. Тогда разработчики действительно будут их запускать. Наши модульные тесты именно так себя и ведут, 1700 тестов отрабатывают за 2 минуты. С функциональными тестами все не так просто, ведь каждый тест предполагает загрузку страницы (зачастую нескольких) с последующей проверкой. Это занимает от нескольких до десятков секунд. Но с учётом того, что всего функциональных тестов у нас более 2000, прогон всех тестов на одной машине занимает порядка 7 часов. Очевидно, что еще не родился такой разработчик, которому хватит терпения дождаться прохождения всех функциональных тестов. Поэтому разработчик запускает или только что написанные тесты, или те, которые тестируют модифицированные страницы. После этого разработчик делает коммит, и в дело вступает билд-сервер, который, общаясь с системой контроля версий, понимает, что появилось что-то новенькое и добавляет коммит в очередь. Очередь разгребается агентами — физическими или виртуальными машинами, смысл существования которых заключается в том, чтобы прогонять весь набор тестов по каждому коммиту. Мы используем 20 агентов для того чтобы проверять корректность работы во всех значимых для нас браузерах. Можно легко увидеть список упавших тестов и начать их поднимать. При этом у нас есть несколько веток в системе контроля версий (стабильная и ветки, в которых мы делаем новые фичи). И на каждую крупную ветку мы напускаем агентов.



Отдельного упоминания должно удостоиться время, когда мы поднимаем тесты. Затягивать тут нельзя. Я слышал, что в некоторых командах в Майкрософте вся работа останавливается и не возобновляется, пока есть хотя бы один упавший тест. Так делать мы, честно говоря, не пробовали и поднимаем тесты ежедневно (этим занимается дежурный инженер), а перед каждым релизом, во время стабилизации (в тяжелых случаях — вся команда).

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

Парное программирование


Мы пишем почти весь код в парах и стремимся к общему владению кодом. То есть у нас нет разделения: серверный программист, клиентский или, скажем, разработчик БД. Любой член команды может взять любую задачу. Понятно, что компетентность и вовлеченность в проект или в какую-то его область у всех разная, поэтому чтобы распространить знания мы используем пары. А ещё вдвоем гораздо веселее. Честно. А самое главное, что это ничуть не медленнее — при работе в паре разработчики вообще не отвлекаются на посторонние дела.

Pre-commit code review


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

Планирование


Это, конечно, нельзя назвать инженерной практикой в чистом виде, просто планирование настолько важная часть жизни команды разработки, что не говорить о нем совершенно невозможно. Все, о чем я писал выше, было связано с качеством, но качество не может быть единственной целью. Хороший код, множество тестов и релизы, которые очень стабильны, но происходят раз в полгода или реже, быстро заведут проект в тупик. Это может работать в заказной разработке, если у вас есть клиент, понимающий что он хочет, готовый за это хорошо заплатить и ожидающий просто качественного воплощения своих желаний. Во всех остальных случаях нужны более частые релизы, чтобы была возможность проверить правильность своих идей и подкорректировать их. В итоге нужно понимать, что команда сумеет сделать в ближайшие несколько недель с высокой степенью достоверности. А парное программирование и общее владение кодом требует, чтобы до начала работы все разработчики понимали задачи одинаково. Мы это обеспечиваем с помощью planning poker — широкоизвестной практики команд, применяющих гибкие методологии разработки. Классический подход состоит в том, что команда набирает несколько задач в очередную итерацию разработки (мы используем двухнедельные итерации) и голосует сколько «идеальных» часов займет каждая задача. Наше ноухау состоит в инновационной форме представления результатов голосования, мы используем вот такой mindmap:



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

Баги


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

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

Рефакторинг


Какой русский программист не любит рефакторинг? Несмотря на то, что наличие тестов позволяет довольно спокойно проводить любой рефакторинг, всё равно не всегда удаётся уделить этому достаточное количество времени по той же причине, по которой растёт количество багов, — релиз раз в месяц. Если баг можно закрыть (или не закрыть), ничего при этом не потеряв (и даже не демотивироваться), то ввязавшись в рефакторинг, порой отступить уже нельзя и некуда. Чтобы решить данную проблему, ещё один день в итерацию выделяется под «день свободного творчества», в который можно делать всё, что угодно: рефакторить, делать какие-нибудь магические вещи. А кто-то в этот день, как это не парадоксально, по собственному желанию фиксит баги!

Дежурный инженер


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

Релиз-инженер


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

На самом деле, как правило, дежурный инженер и релиз-инженер — один человек.

Результат


Действуя таким образом, нам удается уже почти 2 года делать 1 значимый релиз в месяц. Система развивается очень динамично, но мы не боимся делать несколько апдейтов в неделю (не все удается запланировать: законодательство меняется слишком стремительно и рост пользователей в последние полгода произошел очень существенный), наши инженерные практики делают разработку очень управляемой и прогнозируемой.
Технические детали
IDE: Microsoft VisualStudio + JetBrains ReSharper
Continuous Integration сервер: JetBrains TeamCity
Багтрэкер: JetBrains YouTrack
Функциональные тесты: Selenium
VCS: Mercurial
Web-сервер: Microsoft IIS + nginx
БД: MS SQL Server

P.S. Может быть, это не очевидно из текста, но у нас есть тестировщик. Он все время загружен работой и работает очень хорошо :)
P.P.S. Спасибо горячо нами любимой компании ДжетБрейнс за отличные инструменты :)
Tags:
Hubs:
+77
Comments 41
Comments Comments 41

Articles

Information

Website
tech.kontur.ru
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия